init import from old mailr project (http://svn.littlegreen.org/mailr/trunk)

This commit is contained in:
Eugene Korbut 2009-01-08 05:27:12 +10:00
commit 51b79e7298
640 changed files with 34651 additions and 0 deletions

718
CHANGELOG Normal file
View file

@ -0,0 +1,718 @@
*0.14.2 (RC3)* (October 26th, 2005)
* Constants set in the development/test/production environment file are set in Object
* Scaffold generator pays attention to the controller name. #2562 [self@mattmower.com]
* Include tasks from vendor/plugins/*/tasks in the Rakefile #2545 [Rick Olson]
*0.14.1 (RC2)* (October 19th, 2005)
* Don't clean RAILS_ROOT on windows
* Remove trailing '/' from RAILS_ROOT [Nicholas Seckar]
* Upgraded to Active Record 1.12.1 and Action Pack 1.10.1
*0.14.0 (RC1)* (October 16th, 2005)
* Moved generator folder from RAILS_ROOT/generators to RAILS_ROOT/lib/generators [Tobias Luetke]
* Fix rake dev and related commands [Nicholas Seckar]
* The rails command tries to deduce your MySQL socket by running `mysql_config
--socket`. If it fails, default to /path/to/your/mysql.sock
* Made the rails command use the application name for database names in the tailored database.yml file. Example: "rails ~/projects/blog" will use "blog_development" instead of "rails_development". [Florian Weber]
* Added Rails framework freezing tasks: freeze_gems (freeze to current gems), freeze_edge (freeze to Rails SVN trunk), unfreeze_rails (float with newest gems on system)
* Added update_javascripts task which will fetch all the latest js files from your current rails install. Use after updating rails. [Tobias Luetke]
* Added cleaning of RAILS_ROOT to useless elements such as '../non-dot-dot/'. Provides cleaner backtraces and error messages. [Nicholas Seckar]
* Made the instantiated/transactional fixtures settings be controlled through Rails::Initializer. Transactional and non-instantiated fixtures are default from now on. [Florian Weber]
* Support using different database adapters for development and test with ActiveRecord::Base.schema_format = :ruby [Sam Stephenson]
* Make webrick work with session(:off)
* Add --version, -v option to the Rails command. Closes #1840. [stancell]
* Update Prototype to V1.4.0_pre11, script.aculo.us to V1.5_rc3 [2504] and fix the rails generator to include the new .js files [Thomas Fuchs]
* Make the generator skip a file if it already exists and is identical to the new file.
* Add experimental plugin support #2335
* Made Rakefile aware of new .js files in script.aculo.us [Thomas Fuchs]
* Make table_name and controller_name in generators honor AR::Base.pluralize_table_names. #1216 #2213 [kazuhiko@fdiary.net]
* Clearly label functional and unit tests in rake stats output. #2297 [lasse.koskela@gmail.com]
* Make the migration generator only check files ending in *.rb when calculating the next file name #2317 [Chad Fowler]
* Added prevention of duplicate migrations from the generator #2240 [fbeausoleil@ftml.net]
* Add db_schema_dump and db_schema_import rake tasks to work with the new ActiveRecord::SchemaDumper (for dumping a schema to and reading a schema from a ruby file).
* Reformed all the config/environments/* files to conform to the new Rails::Configuration approach. Fully backwards compatible.
* Added create_sessions_table, drop_sessions_table, and purge_sessions_table as rake tasks for databases that supports migrations (MySQL, PostgreSQL, SQLite) to get a table for use with CGI::Session::ActiveRecordStore
* Added dump of schema version to the db_structure_dump task for databases that support migrations #1835 [Rick Olson]
* Fixed script/profiler for Ruby 1.8.2 #1863 [Rick Olson]
* Fixed clone_structure_to_test task for SQLite #1864 [jon@burningbush.us]
* Added -m/--mime-types option to the WEBrick server, so you can specify a Apache-style mime.types file to load #2059 [ask@develooper.com]
* Added -c/--svn option to the generator that'll add new files and remove destroyed files using svn add/revert/remove as appropriate #2064 [kevin.clark@gmail.com]
* Added -c/--charset option to WEBrick server, so you can specify a default charset (which without changes is UTF-8) #2084 [wejn@box.cz]
* Make the default stats task extendable by modifying the STATS_DIRECTORIES constant
* Allow the selected environment to define RAILS_DEFAULT_LOGGER, and have Rails::Initializer use it if it exists.
* Moved all the shared tasks from Rakefile into Rails, so that the Rakefile is empty and doesn't require updating.
* Added Rails::Initializer and Rails::Configuration to abstract all of the common setup out of config/environment.rb (uses config/boot.rb to bootstrap the initializer and paths)
* Fixed the scaffold generator to fail right away if the database isn't accessible instead of in mid-air #1169 [Chad Fowler]
* Corrected project-local generator location in scripts.rb #2010 [Michael Schuerig]
* Don't require the environment just to clear the logs #2093 [Scott Barron]
* Make the default rakefile read *.rake files from config/tasks (for easy extension of the rakefile by e.g. generators)
* Only load breakpoint in development mode and when BREAKPOINT_SERVER_PORT is defined.
* Allow the --toggle-spin switch on process/reaper to be negated
* Replace render_partial with render :partial in scaffold generator [Nicholas Seckar]
* Added -w flag to ps in process/reaper #1934 [Scott Barron]
* Allow ERb in the database.yml file (just like with fixtures), so you can pull out the database configuration in environment variables #1822 [Duane Johnson]
* Added convenience controls for FCGI processes (especially when managed remotely): spinner, spawner, and reaper. They reside in script/process. More details can be had by calling them with -h/--help.
* Added load_fixtures task to the Rakefile, which will load all the fixtures into the database for the current environment #1791 [Marcel Molina]
* Added an empty robots.txt to public/, so that web servers asking for it won't trigger a dynamic call, like favicon.ico #1738 [michael@schubert]
* Dropped the 'immediate close-down' of FCGI processes since it didn't work consistently and produced bad responses when it didn't. So now a TERM ensures exit after the next request (just as if the process is handling a request when it receives the signal). This means that you'll have to 'nudge' all FCGI processes with a request in order to ensure that they have all reloaded. This can be done by something like ./script/process/repear --nudge 'http://www.myapp.com' --instances 10, which will load the myapp site 10 times (and thus hit all of the 10 FCGI processes once, enough to shut down).
*0.13.1* (11 July, 2005)
* Look for app-specific generators in RAILS_ROOT/generators rather than the clunky old RAILS_ROOT/script/generators. Nobody really uses this feature except for the unit tests, so it's a negligible-impact change. If you want to work with third-party generators, drop them in ~/.rails/generators or simply install gems.
* Fixed that each request with the WEBrick adapter would open a new database connection #1685 [Sam Stephenson]
* Added support for SQL Server in the database rake tasks #1652 [ken.barker@gmail.com] Note: osql and scptxfr may need to be installed on your development environment. This involves getting the .exes and a .rll (scptxfr) from a production SQL Server (not developer level SQL Server). Add their location to your Environment PATH and you are all set.
* Added a VERSION parameter to the migrate task that allows you to do "rake migrate VERSION=34" to migrate to the 34th version traveling up or down depending on the current version
* Extend Ruby version check to include RUBY_RELEASE_DATE >= '2005-12-25', the final Ruby 1.8.2 release #1674 [court3nay@gmail.com]
* Improved documentation for environment config files #1625 [court3nay@gmail.com]
*0.13.0* (6 July, 2005)
* Changed the default logging level in config/environment.rb to INFO for production (so SQL statements won't be logged)
* Added migration generator: ./script/generate migration add_system_settings
* Added "migrate" as rake task to execute all the pending migrations from db/migrate
* Fixed that model generator would make fixtures plural, even if ActiveRecord::Base.pluralize_table_names was false #1185 [Marcel Molina]
* Added a DOCTYPE of HTML transitional to the HTML files generated by Rails #1124 [Michael Koziarski]
* SIGTERM also gracefully exits dispatch.fcgi. Ignore SIGUSR1 on Windows.
* Add the option to manually manage garbage collection in the FastCGI dispatcher. Set the number of requests between GC runs in your public/dispatch.fcgi [skaes@web.de]
* Allow dynamic application reloading for dispatch.fcgi processes by sending a SIGHUP. If the process is currently handling a request, the request will be allowed to complete first. This allows production fcgi's to be reloaded without having to restart them.
* RailsFCGIHandler (dispatch.fcgi) no longer tries to explicitly flush $stdout (CgiProcess#out always calls flush)
* Fixed rakefile actions against PostgreSQL when the password is all numeric #1462 [michael@schubert.cx]
* ActionMailer::Base subclasses are reloaded with the other rails components #1262
* Made the WEBrick adapter not use a mutex around action performance if ActionController::Base.allow_concurrency is true (default is false)
* Fixed that mailer generator generated fixtures/plural while units expected fixtures/singular #1457 [Scott Barron]
* Added a 'whiny nil' that's aim to ensure that when users pass nil to methods where that isn't appropriate, instead of NoMethodError? and the name of some method used by the framework users will see a message explaining what type of object was expected. Only active in test and development environments by default #1209 [Michael Koziarski]
* Fixed the test_helper.rb to be safe for requiring controllers from multiple spots, like app/controllers/article_controller.rb and app/controllers/admin/article_controller.rb, without reloading the environment twice #1390 [Nicholas Seckar]
* Fixed Webrick to escape + characters in URL's the same way that lighttpd and apache do #1397 [Nicholas Seckar]
* Added -e/--environment option to script/runner #1408 [fbeausoleil@ftml.net]
* Modernize the scaffold generator to use the simplified render and test methods and to change style from @params["id"] to params[:id]. #1367
* Added graceful exit from pressing CTRL-C during the run of the rails command #1150 [Caleb Tennis]
* Allow graceful exits for dispatch.fcgi processes by sending a SIGUSR1. If the process is currently handling a request, the request will be allowed to complete and then will terminate itself. If a request is not being handled, the process is terminated immediately (via #exit). This basically works like restart graceful on Apache. [Jamis Buck]
* Made dispatch.fcgi more robust by catching fluke errors and retrying unless its a permanent condition. [Jamis Buck]
* Added console --profile for profiling an IRB session #1154 [Jeremy Kemper]
* Changed console_sandbox into console --sandbox #1154 [Jeremy Kemper]
*0.12.1* (20th April, 2005)
* Upgraded to Active Record 1.10.1, Action Pack 1.8.1, Action Mailer 0.9.1, Action Web Service 0.7.1
*0.12.0* (19th April, 2005)
* Fixed that purge_test_database would use database settings from the development environment when recreating the test database #1122 [rails@cogentdude.com]
* Added script/benchmarker to easily benchmark one or more statement a number of times from within the environment. Examples:
# runs the one statement 10 times
script/benchmarker 10 'Person.expensive_method(10)'
# pits the two statements against each other with 50 runs each
script/benchmarker 50 'Person.expensive_method(10)' 'Person.cheap_method(10)'
* Added script/profiler to easily profile a single statement from within the environment. Examples:
script/profiler 'Person.expensive_method(10)'
script/profiler 'Person.expensive_method(10)' 10 # runs the statement 10 times
* Added Rake target clear_logs that'll truncate all the *.log files in log/ to zero #1079 [Lucas Carlson]
* Added lazy typing for generate, such that ./script/generate cn == ./script/generate controller and the likes #1051 [k@v2studio.com]
* Fixed that ownership is brought over in pg_dump during tests for PostgreSQL #1060 [pburleson@gmail.com]
* Upgraded to Active Record 1.10.0, Action Pack 1.8.0, Action Mailer 0.9.0, Action Web Service 0.7.0, Active Support 1.0.4
*0.11.1* (27th March, 2005)
* Fixed the dispatch.fcgi use of a logger
* Upgraded to Active Record 1.9.1, Action Pack 1.7.0, Action Mailer 0.8.1, Action Web Service 0.6.2, Active Support 1.0.3
*0.11.0* (22th March, 2005)
* Removed SCRIPT_NAME from the WEBrick environment to prevent conflicts with PATH_INFO #896 [Nicholas Seckar]
* Removed ?$1 from the dispatch.f/cgi redirect line to get rid of 'complete/path/from/request.html' => nil being in the @params now that the ENV["REQUEST_URI"] is used to determine the path #895 [dblack/Nicholas Seckar]
* Added additional error handling to the FastCGI dispatcher to catch even errors taking down the entire process
* Improved the generated scaffold code a lot to take advantage of recent Rails developments #882 [Tobias Luetke]
* Combined the script/environment.rb used for gems and regular files version. If vendor/rails/* has all the frameworks, then files version is used, otherwise gems #878 [Nicholas Seckar]
* Changed .htaccess to allow dispatch.* to be called from a sub-directory as part of the push with Action Pack to make Rails work on non-vhost setups #826 [Nicholas Seckar/Tobias Luetke]
* Added script/runner which can be used to run code inside the environment by eval'ing the first parameter. Examples:
./script/runner 'ReminderService.deliver'
./script/runner 'Mailer.receive(STDIN.read)'
This makes it easier to do CRON and postfix scripts without actually making a script just to trigger 1 line of code.
* Fixed webrick_server cookie handling to allow multiple cookes to be set at once #800, #813 [dave@cherryville.org]
* Fixed the Rakefile's interaction with postgresql to:
1. Use PGPASSWORD and PGHOST in the environment to fix prompting for
passwords when connecting to a remote db and local socket connections.
2. Add a '-x' flag to pg_dump which stops it dumping privileges #807 [rasputnik]
3. Quote the user name and use template0 when dumping so the functions doesn't get dumped too #855 [pburleson]
4. Use the port if available #875 [madrobby]
* Upgraded to Active Record 1.9.0, Action Pack 1.6.0, Action Mailer 0.8.0, Action Web Service 0.6.1, Active Support 1.0.2
*0.10.1* (7th March, 2005)
* Fixed rake stats to ignore editor backup files like model.rb~ #791 [skanthak]
* Added exception shallowing if the DRb server can't be started (not worth making a fuss about to distract new users) #779 [Tobias Luetke]
* Added an empty favicon.ico file to the public directory of new applications (so the logs are not spammed by its absence)
* Fixed that scaffold generator new template should use local variable instead of instance variable #778 [Dan Peterson]
* Allow unit tests to run on a remote server for PostgreSQL #781 [adamm@galacticasoftware.com]
* Added web_service generator (run ./script/generate web_service for help) #776 [Leon Bredt]
* Added app/apis and components to code statistics report #729 [Scott Barron]
* Fixed WEBrick server to use ABSOLUTE_RAILS_ROOT instead of working_directory #687 [Nicholas Seckar]
* Fixed rails_generator to be usable without RubyGems #686 [Cristi BALAN]
* Fixed -h/--help for generate and destroy generators #331
* Added begin/rescue around the FCGI dispatcher so no uncaught exceptions can bubble up to kill the process (logs to log/fastcgi.crash.log)
* Fixed that association#count would produce invalid sql when called sequentialy #659 [kanis@comcard.de]
* Fixed test/mocks/testing to the correct test/mocks/test #740
* Added early failure if the Ruby version isn't 1.8.2 or above #735
* Removed the obsolete -i/--index option from the WEBrick servlet #743
* Upgraded to Active Record 1.8.0, Action Pack 1.5.1, Action Mailer 0.7.1, Action Web Service 0.6.0, Active Support 1.0.1
*0.10.0* (24th February, 2005)
* Changed default IP binding for WEBrick from 127.0.0.1 to 0.0.0.0 so that the server is accessible both locally and remotely #696 [Marcel]
* Fixed that script/server -d was broken so daemon mode couldn't be used #687 [Nicholas Seckar]
* Upgraded to breakpoint 92 which fixes:
* overload IRB.parse_opts(), fixes #443
=> breakpoints in tests work even when running them via rake
* untaint handlers, might fix an issue discussed on the Rails ML
* added verbose mode to breakpoint_client
* less noise caused by breakpoint_client by default
* ignored TerminateLineInput exception in signal handler
=> quiet exit on Ctrl-C
* Added support for independent components residing in /components. Example:
Controller: components/list/items_controller.rb
(holds a List::ItemsController class with uses_component_template_root called)
Model : components/list/item.rb
(namespace is still shared, so an Item model in app/models will take precedence)
Views : components/list/items/show.rhtml
* Added --sandbox option to script/console that'll roll back all changes made to the database when you quit #672 [Jeremy Kemper]
* Added 'recent' as a rake target that'll run tests for files that changed in the last 10 minutes #612 [Jeremy Kemper]
* Changed script/console to default to development environment and drop --no-inspect #650 [Jeremy Kemper]
* Added that the 'fixture :posts' syntax can be used for has_and_belongs_to_many fixtures where a model doesn't exist #572 [Jeremy Kemper]
* Added that running test_units and test_functional now performs the clone_structure_to_test as well #566 [rasputnik]
* Added new generator framework that informs about its doings on generation and enables updating and destruction of generated artifacts. See the new script/destroy and script/update for more details #487 [Jeremy Kemper]
* Added Action Web Service as a new add-on framework for Action Pack [Leon Bredt]
* Added Active Support as an independent utility and standard library extension bundle
* Upgraded to Active Record 1.7.0, Action Pack 1.5.0, Action Mailer 0.7.0
*0.9.5* (January 25th, 2005)
* Fixed dependency reloading by switching to a remove_const approach where all Active Records, Active Record Observers, and Action Controllers are reloading by undefining their classes. This enables you to remove methods in all three types and see the change reflected immediately and it fixes #539. This also means that only those three types of classes will benefit from the const_missing and reloading approach. If you want other classes (like some in lib/) to reload, you must use require_dependency to do it.
* Added Florian Gross' latest version of Breakpointer and friends that fixes a variaty of bugs #441 [Florian Gross]
* Fixed skeleton Rakefile to work with sqlite3 out of the box #521 [rasputnik]
* Fixed that script/breakpointer didn't get the Ruby path rewritten as the other scripts #523 [brandt@kurowski.net]
* Fixed handling of syntax errors in models that had already been succesfully required once in the current interpreter
* Fixed that models that weren't referenced in associations weren't being reloaded in the development mode by reinstating the reload
* Fixed that generate scaffold would produce bad functional tests
* Fixed that FCGI can also display SyntaxErrors
* Upgraded to Active Record 1.6.0, Action Pack 1.4.0
*0.9.4.1* (January 18th, 2005)
* Added 5-second timeout to WordNet alternatives on creating reserved-word models #501 [Marcel Molina]
* Fixed binding of caller #496 [Alexey]
* Upgraded to Active Record 1.5.1, Action Pack 1.3.1, Action Mailer 0.6.1
*0.9.4* (January 17th, 2005)
* Added that ApplicationController will catch a ControllerNotFound exception if someone attempts to access a url pointing to an unexisting controller [Tobias Luetke]
* Flipped code-to-test ratio around to be more readable #468 [Scott Baron]
* Fixed log file permissions to be 666 instead of 777 (so they're not executable) #471 [Lucas Carlson]
* Fixed that auto reloading would some times not work or would reload the models twice #475 [Tobias Luetke]
* Added rewrite rules to deal with caching to public/.htaccess
* Added the option to specify a controller name to "generate scaffold" and made the default controller name the plural form of the model.
* Added that rake clone_structure_to_test, db_structure_dump, and purge_test_database tasks now pick up the source database to use from
RAILS_ENV instead of just forcing development #424 [Tobias Luetke]
* Fixed script/console to work with Windows (that requires the use of irb.bat) #418 [octopod]
* Fixed WEBrick servlet slowdown over time by restricting the load path reloading to mod_ruby
* Removed Fancy Indexing as a default option on the WEBrick servlet as it made it harder to use various caching schemes
* Upgraded to Active Record 1.5, Action Pack 1.3, Action Mailer 0.6
*0.9.3* (January 4th, 2005)
* Added support for SQLite in the auto-dumping/importing of schemas for development -> test #416
* Added automated rewriting of the shebang lines on installs through the gem rails command #379 [Manfred Stienstra]
* Added ActionMailer::Base.deliver_method = :test to the test environment so that mail objects are available in ActionMailer::Base.deliveries
for functional testing.
* Added protection for creating a model through the generators with a name of an existing class, like Thread or Date.
It'll even offer you a synonym using wordnet.princeton.edu as a look-up. No, I'm not kidding :) [Florian Gross]
* Fixed dependency management to happen in a unified fashion for Active Record and Action Pack using the new Dependencies module. This means that
the environment options needs to change from:
Before in development.rb:
ActionController::Base.reload_dependencies = true  
ActiveRecord::Base.reload_associations     = true
Now in development.rb:
Dependencies.mechanism = :load
Before in production.rb and test.rb:
ActionController::Base.reload_dependencies = false
ActiveRecord::Base.reload_associations     = false
Now in production.rb and test.rb:
Dependencies.mechanism = :require
* Fixed problems with dependency caching and controller hierarchies on Ruby 1.8.2 in development mode #351
* Fixed that generated action_mailers doesnt need to require the action_mailer since thats already done in the environment #382 [Lucas Carlson]
* Upgraded to Action Pack 1.2.0 and Active Record 1.4.0
*0.9.2*
* Fixed CTRL-C exists from the Breakpointer to be a clean affair without error dumping [Kent Sibilev]
* Fixed "rake stats" to work with sub-directories in models and controllers and to report the code to test ration [Scott Baron]
* Added that Active Record associations are now reloaded instead of cleared to work with the new const_missing hook in Active Record.
* Added graceful handling of an inaccessible log file by redirecting output to STDERR with a warning #330 [rainmkr]
* Added support for a -h/--help parameter in the generator #331 [Ulysses]
* Fixed that File.expand_path in config/environment.rb would fail when dealing with symlinked public directories [mjobin]
* Upgraded to Action Pack 1.1.0 and Active Record 1.3.0
*0.9.1*
* Upgraded to Action Pack 1.0.1 for important bug fix
* Updated gem dependencies
*0.9.0*
* Renamed public/dispatch.servlet to script/server -- it wasn't really dispatching anyway as its delegating calls to public/dispatch.rb
* Renamed AbstractApplicationController and abstract_application.rb to ApplicationController and application.rb, so that it will be possible
for the framework to automatically pick up on app/views/layouts/application.rhtml and app/helpers/application.rb
* Added script/console that makes it even easier to start an IRB session for interacting with the domain model. Run with no-args to
see help.
* Added breakpoint support through the script/breakpointer client. This means that you can break out of execution at any point in
the code, investigate and change the model, AND then resume execution! Example:
class WeblogController < ActionController::Base
def index
@posts = Post.find_all
breakpoint "Breaking out from the list"
end
end
So the controller will accept the action, run the first line, then present you with a IRB prompt in the breakpointer window.
Here you can do things like:
Executing breakpoint "Breaking out from the list" at .../webrick_server.rb:16 in 'breakpoint'
>> @posts.inspect
=> "[#<Post:0x14a6be8 @attributes={\"title\"=>nil, \"body\"=>nil, \"id\"=>\"1\"}>,
#<Post:0x14a6620 @attributes={\"title\"=>\"Rails you know!\", \"body\"=>\"Only ten..\", \"id\"=>\"2\"}>]"
>> @posts.first.title = "hello from a breakpoint"
=> "hello from a breakpoint"
...and even better is that you can examine how your runtime objects actually work:
>> f = @posts.first
=> #<Post:0x13630c4 @attributes={"title"=>nil, "body"=>nil, "id"=>"1"}>
>> f.
Display all 152 possibilities? (y or n)
Finally, when you're ready to resume execution, you press CTRL-D
* Changed environments to be configurable through an environment variable. By default, the environment is "development", but you
can change that and set your own by configuring the Apache vhost with a string like (mod_env must be available on the server):
SetEnv RAILS_ENV production
...if you're using WEBrick, you can pick the environment to use with the command-line parameters -e/--environment, like this:
ruby public/dispatcher.servlet -e production
* Added a new default environment called "development", which leaves the production environment to be tuned exclusively for that.
* Added a start_server in the root of the Rails application to make it even easier to get started
* Fixed public/.htaccess to use RewriteBase and share the same rewrite rules for all the dispatch methods
* Fixed webrick_server to handle requests in a serialized manner (the Rails reloading infrastructure is not thread-safe)
* Added support for controllers in directories. So you can have:
app/controllers/account_controller.rb # URL: /account/
app/controllers/admin/account_controller.rb # URL: /admin/account/
NOTE: You need to update your public/.htaccess with the new rules to pick it up
* Added reloading for associations and dependencies under cached environments like FastCGI and mod_ruby. This makes it possible to use
those environments for development. This is turned on by default, but can be turned off with
ActiveRecord::Base.reload_associations = false and ActionController::Base.reload_dependencies = false in production environments.
* Added support for sub-directories in app/models. So now you can have something like Basecamp with:
app/models/accounting
app/models/project
app/models/participants
app/models/settings
It's poor man's namespacing, but only for file-system organization. You still require files just like before.
Nothing changes inside the files themselves.
* Fixed a few references in the tests generated by new_mailer [Jeremy Kemper]
* Added support for mocks in testing with test/mocks
* Cleaned up the environments a bit and added global constant RAILS_ROOT
*0.8.5* (9)
* Made dev-util available to all tests, so you can insert breakpoints in any test case to get an IRB prompt at that point [Jeremy Kemper]:
def test_complex_stuff
@david.projects << @new_project
breakpoint "Let's have a closer look at @david"
end
You need to install dev-utils yourself for this to work ("gem install dev-util").
* Added shared generator behavior so future upgrades should be possible without manually copying over files [Jeremy Kemper]
* Added the new helper style to both controller and helper templates [Jeremy Kemper]
* Added new_crud generator for creating a model and controller at the same time with explicit scaffolding [Jeremy Kemper]
* Added configuration of Test::Unit::TestCase.fixture_path to test_helper to concide with the new AR fixtures style
* Fixed that new_model was generating singular table/fixture names
* Upgraded to Action Mailer 0.4.0
* Upgraded to Action Pack 0.9.5
* Upgraded to Active Record 1.1.0
*0.8.0 (15)*
* Removed custom_table_name option for new_model now that the Inflector is as powerful as it is
* Changed the default rake action to just do testing and separate API generation and coding statistics into a "doc" task.
* Fixed WEBrick dispatcher to handle missing slashes in the URLs gracefully [alexey]
* Added user option for all postgresql tool calls in the rakefile [elvstone]
* Fixed problem with running "ruby public/dispatch.servlet" instead of "cd public; ruby dispatch.servlet" [alexey]
* Fixed WEBrick server so that it no longer hardcodes the ruby interpreter used to "ruby" but will get the one used based
on the Ruby runtime configuration. [Marcel Molina Jr.]
* Fixed Dispatcher so it'll route requests to magic_beans to MagicBeansController/magic_beans_controller.rb [Caio Chassot]
* "new_controller MagicBeans" and "new_model SubscriptionPayments" will now both behave properly as they use the new Inflector.
* Fixed problem with MySQL foreign key constraint checks in Rake :clone_production_structure_to_test target [Andreas Schwarz]
* Changed WEBrick server to by default be auto-reloading, which is slower but makes source changes instant.
Class compilation cache can be turned on with "-c" or "--cache-classes".
* Added "-b/--binding" option to WEBrick dispatcher to bind the server to a specific IP address (default: 127.0.0.1) [Kevin Temp]
* dispatch.fcgi now DOESN'T set FCGI_PURE_RUBY as it was slowing things down for now reason [Andreas Schwarz]
* Added new_mailer generator to work with Action Mailer
* Included new framework: Action Mailer 0.3
* Upgraded to Action Pack 0.9.0
* Upgraded to Active Record 1.0.0
*0.7.0*
* Added an optional second argument to the new_model script that allows the programmer to specify the table name,
which will used to generate a custom table_name method in the model and will also be used in the creation of fixtures.
[Kevin Radloff]
* script/new_model now turns AccountHolder into account_holder instead of accountholder [Kevin Radloff]
* Fixed the faulty handleing of static files with WEBrick [Andreas Schwarz]
* Unified function_test_helper and unit_test_helper into test_helper
* Fixed bug with the automated production => test database dropping on PostgreSQL [dhawkins]
* create_fixtures in both the functional and unit test helper now turns off the log during fixture generation
and can generate more than one fixture at a time. Which makes it possible for assignments like:
@people, @projects, @project_access, @companies, @accounts =
create_fixtures "people", "projects", "project_access", "companies", "accounts"
* Upgraded to Action Pack 0.8.5 (locally-scoped variables, partials, advanced send_file)
* Upgraded to Active Record 0.9.5 (better table_name guessing, cloning, find_all_in_collection)
*0.6.5*
* No longer specifies a template for rdoc, so it'll use whatever is default (you can change it in the rakefile)
* The new_model generator will now use the same rules for plural wordings as Active Record
(so Category will give categories, not categorys) [Kevin Radloff]
* dispatch.fcgi now sets FCGI_PURE_RUBY to true to ensure that it's the Ruby version that's loaded [danp]
* Made the GEM work with Windows
* Fixed bug where mod_ruby would "forget" the load paths added when switching between controllers
* PostgreSQL are now supported for the automated production => test database dropping [Kevin Radloff]
* Errors thrown by the dispatcher are now properly handled in FCGI.
* Upgraded to Action Pack 0.8.0 (lots and lots and lots of fixes)
* Upgraded to Active Record 0.9.4 (a bunch of fixes)
*0.6.0*
* Added AbstractionApplicationController as a superclass for all controllers generated. This class can be used
to carry filters and methods that are to be shared by all. It has an accompanying ApplicationHelper that all
controllers will also automatically have available.
* Added environments that can be included from any script to get the full Active Record and Action Controller
context running. This can be used by maintenance scripts or to interact with the model through IRB. Example:
require 'config/environments/production'
for account in Account.find_all
account.recalculate_interests
end
A short migration script for an account model that had it's interest calculation strategy changed.
* Accessing the index of a controller with "/weblog" will now redirect to "/weblog/" (only on Apache, not WEBrick)
* Simplified the default Apache config so even remote requests are served off CGI as a default.
You'll now have to do something specific to activate mod_ruby and FCGI (like using the force urls).
This should make it easier for new comers that start on an external server.
* Added more of the necessary Apache options to .htaccess to make it easier to setup
* Upgraded to Action Pack 0.7.9 (lots of fixes)
* Upgraded to Active Record 0.9.3 (lots of fixes)
*0.5.7*
* Fixed bug in the WEBrick dispatcher that prevented it from getting parameters from the URL
(through GET requests or otherwise)
* Added lib in root as a place to store app specific libraries
* Added lib and vendor to load_path, so anything store within can be loaded directly.
Hence lib/redcloth.rb can be loaded with require "redcloth"
* Upgraded to Action Pack 0.7.8 (lots of fixes)
* Upgraded to Active Record 0.9.2 (minor upgrade)
*0.5.6*
* Upgraded to Action Pack 0.7.7 (multipart form fix)
* Updated the generated template stubs to valid XHTML files
* Ensure that controllers generated are capitalized, so "new_controller TodoLists"
gives the same as "new_controller Todolists" and "new_controller todolists".
*0.5.5*
* Works on Windows out of the box! (Dropped symlinks)
* Added webrick dispatcher: Try "ruby public/dispatch.servlet --help" [Florian Gross]
* Report errors about initialization to browser (instead of attempting to use uninitialized logger)
* Upgraded to Action Pack 0.7.6
* Upgraded to Active Record 0.9.1
* Added distinct 500.html instead of reusing 404.html
* Added MIT license
*0.5.0*
* First public release

23
MIT-LICENSE Normal file
View file

@ -0,0 +1,23 @@
Copyright (c) 2005, 2006 VibrantPlanet Ltd.
Copyright (c) 2005, 2006 Luben Manolov
Copyright (c) 2005, 2006 Nick Penkov
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

26
README Normal file
View file

@ -0,0 +1,26 @@
Installation Guide
Requirements
* Ruby 1.8.2 or higher
* Rails 0.14.2
* MySQL or PostgreSQL with Ruby bindings
* Ruby-GetText? 0.8.0 or higher (http://ponx.s5.xrea.com/hiki/ruby-gettext.html)
Installation
1. Checkout the source code
2. Create database called mailr. Create the database schema using db/schema.mysql.sql or db/schema.pgsql.sql
3. Change the config/database.yml to reflect your newly created database configuration
4. If you need to override some of the default constants used in the application take a look at config/default_site.rb. Then create config/site.rb that contains only the keys which you want to override. Example content of config/site.rb is:
module CDF
LOCALCONFIG = {
:imap_server => 'your.imap.server'
}
end
5. Start your web server and login in the application with your IMAP user and password.

27
README_LOCALIZE Normal file
View file

@ -0,0 +1,27 @@
Here we will describe how to make translations for Mailr
1. Supose we will translate Mailr in French
2. In locale directory create subirectory fr_FR
3. In newly created directory fr_FR create subdirectory LC_MESSAGES
result:
|
|_ locale
|
|- bg_BG
|
|- fr_FR
|
|- LC_MESSAGES
4. Run script for extracting strings from rails application to po files
script/localize
5. Run PoEdit (http://poedit.sourceforge.net/)
6. Make translations to your newlly created file (locale/fr_FR/LC_MESSAGES/messages.po)
7. Compile po file to mo file using PoEdit
NOTE: Switching of locales is made in ApplicationController localize filter - by default mailr looks for HTTP_ACCEPT_LANGUAGE of remote browser and switches translation

10
Rakefile Normal file
View file

@ -0,0 +1,10 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/switchtower.rake, and they will automatically be available to Rake.
require(File.join(File.dirname(__FILE__), 'config', 'boot'))
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
require 'tasks/rails'

View file

@ -0,0 +1,170 @@
# The filters added to this controller will be run for all controllers in the application.
# Likewise will all the methods added be available for all controllers.
class ApplicationController < ActionController::Base
before_filter :localize
before_filter :user_login_filter
before_filter :add_scripts
model :customer
protected
def secure_user?() true end
def secure_cust?() false end
def additional_scripts() "" end
def onload_function() "" end
private
def add_scripts
@additional_scripts = additional_scripts()
@onload_function = onload_function()
end
def user_login_filter
if (secure_user? or secure_cust? )and logged_user.nil?
@session["return_to"] = @request.request_uri
redirect_to :controller=>"/login", :action => "index"
return false
end
end
alias login_required user_login_filter
def logged_user # returns customer id
@session['user']
end
def logged_customer
@session['user']
end
def localize
# We will use instance vars for the locale so we can make use of them in
# the templates.
@charset = 'utf-8'
@headers['Content-Type'] = "text/html; charset=#{@charset}"
# Here is a very simplified approach to extract the prefered language
# from the request. If all fails, just use 'en_EN' as the default.
temp = if @request.env['HTTP_ACCEPT_LANGUAGE'].nil?
[]
else
@request.env['HTTP_ACCEPT_LANGUAGE'].split(',').first.split('-') rescue []
end
language = temp.slice(0)
dialect = temp.slice(1)
@language = language.nil? ? 'en' : language.downcase # default is en
# If there is no dialect use the language code ('en' becomes 'en_EN').
@dialect = dialect.nil? ? @language.upcase : dialect
# The complete locale string consists of
# language_DIALECT (en_EN, en_GB, de_DE, ...)
@locale = "#{@language}_#{@dialect.upcase}"
@htmllang = @language == @dialect ? @language : "#{@language}-#{@dialect}"
# Finally, bind the textdomain to the locale. From now on every used
# _('String') will get translated into the right language. (Provided
# that we have a corresponding mo file in the right place).
bindtextdomain('messages', "#{RAILS_ROOT}/locale", @locale, @charset)
end
public
def include_tinymce(mode="textareas",elements="")
tinymce=''
tinymce << '
<script language="javascript" type="text/javascript" src="/tiny_mce/tiny_mce.js"></script>
<script language="javascript" type="text/javascript">
tinyMCE.init({
mode : "'
tinymce << mode << '",'
if mode == "exact"
tinymce << 'elements : "' << elements << '",
'
end
tinymce << '
theme : "advanced",
cleanup : true,
width: "100%",
remove_linebreaks : false,
entity_encoding : "named",
relative_urls : false,
plugins : "table,save,advhr,advimage,advlink,iespell,preview,zoom,searchreplace,print,contextmenu,fullscreen,linkattach",
theme_advanced_buttons1_add : "fontselect,fontsizeselect",
theme_advanced_buttons2_add : "separator,preview,zoom",
theme_advanced_buttons2_add_before: "cut,copy,paste,separator,search,replace,separator",
theme_advanced_buttons3_add_before : "tablecontrols,separator",
theme_advanced_buttons3_add : "iespell,forecolor,backcolor,fullscreen",
theme_advanced_source_editor_width : "700",
theme_advanced_source_editor_height : "500",
theme_advanced_styles : "Header 1=header1",
theme_advanced_toolbar_location : "top",
theme_advanced_toolbar_align : "left",
theme_advanced_path_location : "none",
extended_valid_elements : ""
+"a[accesskey|charset|class|coords|href|hreflang|id|lang|name"
+"|onblur|onclick|ondblclick|onfocus|onkeydown|onkeypress|onkeyup"
+"|onmousedown|onmousemove|onmouseout|onmouseover|onmouseup|rel|rev"
+"|shape|style|tabindex|title|target|type],"
+"dd[class|id|lang|onclick|ondblclick|onkeydown|onkeypress|onkeyup"
+"|onmousedown|onmousemove|onmouseout|onmouseover|onmouseup|style|title],"
+"div[align|class|id|lang|onclick"
+"|ondblclick|onkeydown|onkeypress|onkeyup|onmousedown|onmousemove"
+"|onmouseout|onmouseover|onmouseup|style|title],"
+"dl[class|compact|id|lang|onclick|ondblclick|onkeydown"
+"|onkeypress|onkeyup|onmousedown|onmousemove|onmouseout|onmouseover"
+"|onmouseup|style|title],"
+"dt[class|id|lang|onclick|ondblclick|onkeydown|onkeypress|onkeyup"
+"|onmousedown|onmousemove|onmouseout|onmouseover|onmouseup|style|title],"
+"img[align|alt|border|class|height"
+"|hspace|id|ismap|lang|longdesc|name|onclick|ondblclick|onkeydown"
+"|onkeypress|onkeyup|onmousedown|onmousemove|onmouseout|onmouseover"
+"|onmouseup|src|style|title|usemap|vspace|width],"
+"script[charset|defer|language|src|type],"
+"style[lang|media|title|type],"
+"table[align|bgcolor|border|cellpadding|cellspacing|class"
+"|frame|height|id|lang|onclick|ondblclick|onkeydown|onkeypress"
+"|onkeyup|onmousedown|onmousemove|onmouseout|onmouseover|onmouseup|rules"
+"|style|summary|title|width],"
+"td[abbr|align|axis|bgcolor|char|charoff|class"
+"|colspan|headers|height|id|lang|nowrap|onclick"
+"|ondblclick|onkeydown|onkeypress|onkeyup|onmousedown|onmousemove"
+"|onmouseout|onmouseover|onmouseup|rowspan|scope"
+"|style|title|valign|width],"
+"hr[align|class|id|lang|noshade|onclick"
+"|ondblclick|onkeydown|onkeypress|onkeyup|onmousedown|onmousemove"
+"|onmouseout|onmouseover|onmouseup|size|style|title|width],"
+"font[class|color|face|id|lang|size|style|title],"
+"span[align|class|class|id|lang|onclick|ondblclick|onkeydown"
+"|onkeypress|onkeyup|onmousedown|onmousemove|onmouseout|onmouseover"
+"|onmouseup|style|title]",
external_link_list_url : "/cms/urlchoose/choose_tinymce",
external_attachments_list_url : "/attachments/attachments/choose_tinymce",
external_image_list_url : "/gallery/imgchoose/choose_tinymce",
flash_external_list_url : "example_data/example_flash_list.js"
});
</script>'
tinymce
end
helper_method :include_tinymce
def include_simple_tinymce(mode="textareas",elements="")
tinymce = ''
tinymce << '<script language="javascript" type="text/javascript" src="/tiny_mce/tiny_mce.js"></script>
<script language="javascript" type="text/javascript">
tinyMCE.init({
mode : "'
tinymce << mode << '",'
if mode == "exact"
tinymce << 'elements : "' << elements << '",
'
end
tinymce << '
theme : "default",
width : "100%",
auto_reset_designmode : true
});
</script>'
tinymce
end
helper_method :include_simple_tinymce
end

View file

@ -0,0 +1,67 @@
require 'ezcrypto'
class LoginController < ApplicationController
model :customer
def index
if not(logged_user.nil?)
redirect_to :controller =>"webmail", :action=>"index"
else
@login_user = Customer.new
end
end
def authenticate
if user = auth(@params['login_user']["email"], @params['login_user']["password"])
@session["user"] = user.id
if CDF::CONFIG[:crypt_session_pass]
@session["wmp"] = EzCrypto::Key.encrypt_with_password(CDF::CONFIG[:encryption_password], CDF::CONFIG[:encryption_salt], @params['login_user']["password"])
else
# dont use crypt
@session["wmp"] = @params['login_user']["password"]
end
if @session["return_to"]
redirect_to_path(@session["return_to"])
@session["return_to"] = nil
else
redirect_to :action=>"index"
end
else
@login_user = Customer.new
flash["error"] = _('Wrong email or password specified.')
redirect_to :action => "index"
end
end
def logout
reset_session
flash["status"] = _('User successfully logged out')
redirect_to :action => "index"
end
protected
def need_subdomain?() true end
def secure_user?() false end
private
def auth(email, password)
mailbox = IMAPMailbox.new
begin
mailbox.connect(email, password)
rescue
return nil
end
mailbox.disconnect
mailbox = nil
if user = Customer.find_by_email(email)
return user
else
# create record in database
user = Customer.create("email"=>email)
MailPref.create('customer_id' => user.id)
return user
end
end
end

View file

@ -0,0 +1,137 @@
# The methods added to this helper will be available to all templates in the application.
module ApplicationHelper
protected
def format_datetime(datetime)
datetime.strftime "%d.%m.%Y %H:%M"
end
def errors_base(form_name)
errors = instance_variable_get("@#{form_name}").errors.on_base()
errors_out = ""
if errors
errors = [errors] unless errors.is_a? Array
errors.each do |e|
errors_out << "<span class=\"error\">#{e}</span>"
end
end
errors_out
end
# Useful abstraction for form input fields - combines an input field with error message (if any)
# and writes an appropriate style (for errors)
# Usage:
# form_input :text_field, 'postform', 'subject'
# form_input :text_area, 'postform', 'text', 'Please enter text:', 'cols' => 80
# form_input :hidden_field, 'postform', 'topic_id'
def form_input(helper_method, form_name, field_name, prompt = field_name.capitalize, options = {})
case helper_method.to_s
when 'hidden_field'
self.hidden_field(form_name, field_name)
when /^.*button$/
<<-EOL
<tr><td class="button" colspan="2">
#{self.send(helper_method, form_name, prompt, options)}
</td></tr>
EOL
else
field = (
if :select == helper_method
self.send(helper_method, form_name, field_name, options.delete('values'), options)
elsif :collection_select == helper_method
self.send(helper_method, form_name, field_name, options.delete('collection'), options.delete('value_method'), options.delete('text_method'), options)
else
self.send(helper_method, form_name, field_name, options)
end)
errors = instance_variable_get("@#{form_name}").errors[field_name] unless instance_variable_get("@#{form_name}").nil?
errors = Array.new if errors.nil?
errors_out = ""
if errors
errors = [errors] unless errors.is_a? Array
errors.each do |e|
errors_out << "<span class=\"error\">#{e}</span>"
end
end
if options['class'] == 'two_columns'
<<-EOL
<tr class="two_columns">
<td class="prompt"><label>#{prompt}:</label></td>
<td class="value">#{field}#{errors_out}</td>
</tr>
EOL
else
<<-EOL
<tr><td class="prompt"><strong>#{prompt}:</strong></td></tr>
<tr><td class="value">#{field}#{errors_out}</td></tr>
EOL
end
end
end
# Helper method that has the same signature as real input field helpers, but simply displays
# the value of a given field enclosed within <p> </p> tags.
# Usage:
# <%= form_input :read_only_field, 'new_user', 'name', _('user_name')) %>
def read_only_field(form_name, field_name, html_options)
"<span #{attributes(html_options)}>#{instance_variable_get('@' + form_name)[field_name]}</span>"
end
def submit_button(form_name, prompt, html_options)
%{<input name="submit" type="submit" value="#{prompt}" />}
end
# Converts a hash to XML attributes string. E.g.:
# to_attributes('a' => 'aaa', 'b' => 1)
# => 'a="aaa" b="1" '
def attributes(hash)
hash.keys.inject("") { |attrs, key| attrs + %{#{key}="#{hash[key]}" } }
end
def initListClass
@itClass = 1
end
def popListClass
ret = getListClass
@itClass = @itClass + 1
return ret
end
def getListClass
return "even" if @itClass%2 == 0
return "odd" if @itClass%2 == 1
end
def get_meta_info
'<meta name="rating" content="General">'
'<meta name="robots" content="Index, ALL">'
'<meta name="description" content="">'
'<meta name="keywords" content="">'
'<meta name content="">'
end
def user
@user = Customer.find(@session["user"]) if @user.nil?
@user
end
def link_main
link_to(_('Contacts'), :controller => '/contacts/contact', :action => 'list')
end
def alternator
if @alternator.nil?
@alternator = 1
end
@alternator = -@alternator
if @alternator == -1
return "even"
else
return "odd"
end
end
end

View file

@ -0,0 +1,487 @@
# == Pagination Helper
#
# === Action Pack pagination for Active Record collections
#
# <em>Sam Stephenson <sstephenson at gmail dot com></em>
#
# Pagination Helper aids in the process of paging large collections of Active
# Record objects. It offers macro-style automatic fetching of your model for
# multiple views, or explicit fetching for single actions. And if the magic
# isn't flexible enough for your needs, you can create your own paginators
# with a minimal amount of code.
#
# Unlike most helpers, Pagination Helper is available to all of Action Pack:
# it helps you query your models with Action Controller and render links in
# Action View.
#
# ----
#
# === Controller examples
#
# Pagination Helper can handle as much or as little as you wish. In the
# controller, have it automatically query your model for pagination; or,
# if you prefer, create Paginator objects yourself.
#
# ==== Automatic pagination for every action in a controller
#
# class PersonController < ApplicationController
# helper :pagination
# model :person
#
# paginate :people, :order_by => 'last_name, first_name',
# :per_page => 20
#
# # ...
# end
#
# Each action in this controller now has access to a <tt>@people</tt> instance
# variable, which is an ordered collection of model objects for the current
# page (at most 20, sorted by last name and first name), and a
# <tt>@person_pages</tt> Paginator instance. The current page is determined by
# the <tt>@params['page']</tt> variable.
#
# ==== Pagination for a single action
#
# def list
# @person_pages, @people =
# paginate :people, :order_by => 'last_name, first_name'
# end
#
# Like the previous example, but explicitly creates <tt>@person_pages</tt> and
# <tt>@people</tt> for a single action, and uses the default of 10 items per
# page.
#
# ==== Custom/"classic" pagination
#
# def list
# @person_pages = Paginator.new self, Person.count, 10, @params['page']
# @people = Person.find_all nil, 'last_name, first_name',
# @person_pages.current.to_sql
# end
#
# Explicitly creates the paginator from the previous example and uses
# Paginator#to_sql to retrieve <tt>@people</tt> from the model.
#
# === View examples
#
# Paginator Helper includes various methods to help you display pagination
# links in your views. (For information on displaying the paginated
# collections you retrieve in the controller, see the documentation for
# ActionView::Partials#render_collection_of_partials.)
#
# ==== Using Paginator#basic_html
#
# Use the +basic_html+ method to get a simple list of pages (always including
# the first and last page) with a window around the current page. In your
# view, using one of the controller examples above,
#
# <%= @person_pages.basic_html(self) %>
#
# will render a list of links. For a list of parameters, see the documentation
# for Page#basic_html.
#
# ==== Using a paginator partial
#
# If you need more advanced control over pagination links and need to paginate
# multiple actions and controllers, consider using a shared paginator partial.
# For instance, _why suggests this partial, which you might place in
# <tt>app/views/partial/_paginator.rhtml</tt>:
#
# <div class="counter">
# Displaying <%= paginator.current.first_item %>
# - <%= paginator.current.last_item %>
# of <%= paginator.item_count %> &nbsp; &nbsp;
#
# <%= link_to(h('< Previous'), paginator.current.previous.to_link) +
# " | " if paginator.current.previous %>
# <%= paginator.basic_html(self, 4) %>
# <%= " | " + link_to(h('Next >'), paginator.current.next.to_link) if
# paginator.current.next %>
# </div>
#
# Then in your views, simply call
#
# <%= render_partial "partial/paginator", @person_pages %>
#
# wherever you want your pagination links to appear.
#
# ----
#
# ==== Thanks
#
# Thanks to the following people for their contributions to Pagination Helper:
# * Marcel Molina Jr (noradio), who provided the idea for and original
# implementation of Paginator::Window, as well as endless mental support.
# * evl, who pointed out that Page#link_to should take and merge in a hash of
# additional parameters.
# * why the lucky stiff, who wrote a lovely article on the original Pagination
# Helper alongside the first implementation of Paginator#base_html, created
# Page#first_item and Page#last_item, and who provided inspiration for
# revising Pagination Helper to make it more "Rails-ish."
# * xal, who saw the limitations of having only the macro-style paginate
# method and suggested the single-action version, and who also suggested the
# automatic inclusion into ActionController::Base.
# * jbd for feedback and debugging help.
# * ##rubyonrails on Freenode and the Rails mailing list.
#
module PaginationHelper
# A hash holding options for controllers using macro-style pagination
OPTIONS = Hash.new
# The default options for pagination
DEFAULT_OPTIONS = {
:class_name => nil,
:per_page => 10,
:parameter => 'page',
:conditions => nil,
:order_by => nil
}
def self.included(base) #:nodoc:
super
base.extend(ClassMethods)
end
def self.validate_options!(collection_id, options, in_action) #:nodoc:
options.merge!(DEFAULT_OPTIONS) {|key, old, new| old}
valid_options = [:class_name, :per_page,
:parameter, :conditions, :order_by]
valid_options << :actions unless in_action
unknown_option_keys = options.keys - valid_options
raise ActionController::ActionControllerError,
"Unknown options: #{unknown_option_keys.join(', ')}" unless
unknown_option_keys.empty?
options[:singular_name] = Inflector.singularize(collection_id.to_s)
options[:class_name] ||= Inflector.camelize(options[:singular_name])
end
# Returns a paginator and a collection of Active Record model instances for
# the paginator's current page. This is designed to be used in a single
# action; to automatically paginate multiple actions, consider
# ClassMethods#paginate.
#
# +options+ are:
# <tt>:class_name</tt>:: the class name to use, if it can't be inferred by
# singularizing the collection name.
# <tt>:per_page</tt>:: the maximum number of items to include in a
# single page. Defaults to 10.
# <tt>:parameter</tt>:: the CGI parameter from which the current page is
# determined. Defaults to 'page'; i.e., the current
# page is specified by <tt>@params['page']</tt>.
# <tt>:conditions</tt>:: optional conditions passed to Model.find_all.
# <tt>:order_by</tt>:: optional order parameter passed to Model.find_all
# and Model.count.
def paginate(collection_id, options={})
PaginationHelper.validate_options!(collection_id, options, true)
paginator_and_collection_for(collection_id, options)
end
# These methods become class methods on any controller which includes
# PaginationHelper.
module ClassMethods
# Creates a +before_filter+ which automatically paginates an Active Record
# model for all actions in a controller (or certain actions if specified
# with the <tt>:actions</tt> option).
#
# +options+ are the same as PaginationHelper#paginate, with the addition
# of:
# <tt>:actions</tt>:: an array of actions for which the pagination is
# active. Defaults to +nil+ (i.e., every action).
def paginate(collection_id, options={})
PaginationHelper.validate_options!(collection_id, options, false)
module_eval do
before_filter :create_paginators_and_retrieve_collections
OPTIONS[self] ||= Hash.new
OPTIONS[self][collection_id] = options
end
end
end
def create_paginators_and_retrieve_collections #:nodoc:
PaginationHelper::OPTIONS[self.class].each do |collection_id, options|
next unless options[:actions].include? action_name if options[:actions]
paginator, collection =
paginator_and_collection_for(collection_id, options)
paginator_name = "@#{options[:singular_name]}_pages"
self.instance_variable_set(paginator_name, paginator)
collection_name = "@#{collection_id.to_s}"
self.instance_variable_set(collection_name, collection)
end
end
# Returns the total number of items in the collection to be paginated for
# the +model+ and given +conditions+. Override this method to implement a
# custom counter.
def count_collection_for_pagination(model, conditions)
model.count(conditions)
end
# Returns a collection of items for the given +model+ and +conditions+,
# ordered by +order_by+, for the current page in the given +paginator+.
# Override this method to implement a custom finder.
def find_collection_for_pagination(model, conditions, order_by, paginator)
model.find_all(conditions, order_by, paginator.current.to_sql)
end
protected :create_paginators_and_retrieve_collections,
:count_collection_for_pagination, :find_collection_for_pagination
def paginator_and_collection_for(collection_id, options) #:nodoc:
klass = eval options[:class_name]
page = @params[options[:parameter]]
count = count_collection_for_pagination(klass, options[:conditions])
paginator = Paginator.new(self, count,
options[:per_page], page, options[:parameter])
collection = find_collection_for_pagination(klass,
options[:conditions], options[:order_by], paginator)
return paginator, collection
end
private :paginator_and_collection_for
# A class representing a paginator for an Active Record collection.
class Paginator
include Enumerable
# Creates a new Paginator on the given +controller+ for a set of items of
# size +item_count+ and having +items_per_page+ items per page. Raises
# ArgumentError if items_per_page is out of bounds (i.e., less than or
# equal to zero). The page CGI parameter for links defaults to "page" and
# can be overridden with +page_parameter+.
def initialize(controller, item_count, items_per_page, current_page=1,
page_parameter='page')
raise ArgumentError, 'must have at least one item per page' if
items_per_page <= 0
@controller = controller
@item_count = item_count || 0
@items_per_page = items_per_page
@page_parameter = page_parameter
self.current_page = current_page
end
attr_reader :controller, :item_count, :items_per_page, :page_parameter
# Sets the current page number of this paginator. If +page+ is a Page
# object, its +number+ attribute is used as the value; if the page does
# not belong to this Paginator, an ArgumentError is raised.
def current_page=(page)
if page.is_a? Page
raise ArgumentError, 'Page/Paginator mismatch' unless
page.paginator == self
end
page = page.to_i
@current_page = has_page_number?(page) ? page : 1
end
# Returns a Page object representing this paginator's current page.
def current_page
self[@current_page]
end
alias current :current_page
# Returns a new Page representing the first page in this paginator.
def first_page
self[1]
end
alias first :first_page
# Returns a new Page representing the last page in this paginator.
def last_page
self[page_count]
end
alias last :last_page
# Returns the number of pages in this paginator.
def page_count
return 1 if @item_count.zero?
(@item_count / @items_per_page.to_f).ceil
end
alias length :page_count
# Returns true if this paginator contains the page of index +number+.
def has_page_number?(number)
return false unless number.is_a? Fixnum
number >= 1 and number <= page_count
end
# Returns a new Page representing the page with the given index +number+.
def [](number)
Page.new(self, number)
end
# Successively yields all the paginator's pages to the given block.
def each(&block)
page_count.times do |n|
yield self[n+1]
end
end
# Creates a basic HTML link bar for the given +view+. The first and last
# pages of the paginator are always shown, along with +window_size+ pages
# around the current page. By default, the current page is displayed, but
# not linked; to change this behavior, pass true to the
# +link_to_current_page+ argument. Specify additional link_to parameters
# with +params+.
def basic_html(view, window_size=2, link_to_current_page=false, params={})
window_pages = current.window(window_size).pages
return if window_pages.length <= 1 unless link_to_current_page
html = ''
unless window_pages[0].first?
html << view.link_to(first.number, first.to_link(params))
html << ' ... ' if window_pages[0].number - first.number > 1
html << ' '
end
window_pages.each do |page|
if current == page and not link_to_current_page
html << page.number.to_s
else
html << view.link_to(page.number, page.to_link(params))
end
html << ' '
end
unless window_pages.last.last?
html << ' ... ' if last.number - window_pages[-1].number > 1
html << view.link_to(last.number, last.to_link(params))
end
html
end
# A class representing a single page in a paginator.
class Page
include Comparable
# Creates a new Page for the given +paginator+ with the index +number+.
# If +number+ is not in the range of valid page numbers or is not a
# number at all, it defaults to 1.
def initialize(paginator, number)
@paginator = paginator
@number = number.to_i
@number = 1 unless @paginator.has_page_number? @number
end
attr_reader :paginator, :number
alias to_i :number
# Compares two Page objects and returns true when they represent the
# same page (i.e., their paginators are the same and they have the same
# page number).
def ==(page)
@paginator == page.paginator and
@number == page.number
end
# Compares two Page objects and returns -1 if the left-hand page comes
# before the right-hand page, 0 if the pages are equal, and 1 if the
# left-hand page comes after the right-hand page. Raises ArgumentError
# if the pages do not belong to the same Paginator object.
def <=>(page)
raise ArgumentError unless @paginator == page.paginator
@number <=> page.number
end
# Returns the item offset for the first item in this page.
def offset
@paginator.items_per_page * (@number - 1)
end
# Returns the number of the first item displayed.
def first_item
offset + 1
end
# Returns the number of the last item displayed.
def last_item
[@paginator.items_per_page * @number, @paginator.item_count].min
end
# Returns true if this page is the first page in the paginator.
def first?
self == @paginator.first
end
# Returns true if this page is the last page in the paginator.
def last?
self == @paginator.last
end
# Returns a new Page object representing the page just before this page,
# or nil if this is the first page.
def previous
if first? then nil else Page.new(@paginator, @number - 1) end
end
# Returns a new Page object representing the page just after this page,
# or nil if this is the last page.
def next
if last? then nil else Page.new(@paginator, @number + 1) end
end
# Returns a new Window object for this page with the specified
# +padding+.
def window(padding=2)
Window.new(self, padding)
end
# Returns a hash appropriate for using with link_to or url_for. Takes an
# optional +params+ hash for specifying additional link parameters.
def to_link(params={})
{:params => {@paginator.page_parameter => @number.to_s}.merge(params)}
end
# Returns the SQL "LIMIT ... OFFSET" clause for this page.
def to_sql
['? OFFSET ?', @paginator.items_per_page, offset]
end
end
# A class for representing ranges around a given page.
class Window
# Creates a new Window object for the given +page+ with the specified
# +padding+.
def initialize(page, padding=2)
@paginator = page.paginator
@page = page
self.padding = padding
end
attr_reader :paginator, :page
# Sets the window's padding (the number of pages on either side of the
# window page).
def padding=(padding)
@padding = padding < 0 ? 0 : padding
# Find the beginning and end pages of the window
@first = @paginator.has_page_number?(@page.number - @padding) ?
@paginator[@page.number - @padding] : @paginator.first
@last = @paginator.has_page_number?(@page.number + @padding) ?
@paginator[@page.number + @padding] : @paginator.last
end
attr_reader :padding, :first, :last
# Returns an array of Page objects in the current window.
def pages
(@first.number..@last.number).to_a.map {|n| @paginator[n]}
end
alias to_a :pages
end
end
end
module ActionController #:nodoc:
class Base #:nodoc:
# Automatically include PaginationHelper.
include PaginationHelper
end
end

View file

@ -0,0 +1,18 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<%= @htmllang %>" lang="<%= @htmllang %>">
<head>
<title><%=@title%></title>
<meta http-equiv="content-type" content="text/html; charset=<%= @charset %>" />
<script type="text/javascript" src="/javascripts/global.js"></script>
<link rel="stylesheet" href="/stylesheets/admin.css" type="text/css" media="screen" />
<link rel="stylesheet" href="/stylesheets/tabs.css" type="text/css" media="screen" />
<link rel="stylesheet" href="/stylesheets/mailr.css" type="text/css" media="screen" />
<%=@additional_scripts%>
</head>
<body id="bodyID" onload="<%=@onload_function%>">
<%= @content_for_layout %>
</body>
</html>

View file

@ -0,0 +1,25 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<%= @htmllang %>" lang="<%= @htmllang %>">
<head>
<title><%=_('Mailr')%></title>
<meta http-equiv="content-type" content="text/html; charset=<%= @charset %>" />
<link rel="stylesheet" href="/stylesheets/admin.css" type="text/css" media="screen" />
<link rel="stylesheet" href="/stylesheets/tabs.css" type="text/css" media="screen" />
<link rel="stylesheet" href="/stylesheets/mailr.css" type="text/css" media="screen" />
<script type="text/javascript" src="/javascripts/global.js"></script>
<script type="text/javascript" src="/javascripts/prototype.js"></script>
<%=(@content_for_scripts ? @content_for_scripts : @additional_scripts )%>
</head>
<body id="bodyID" onload="<%=@onload_function%>">
<div id="wholepage">
<div id="container">
<div id="sidebar_outer">
<div id="sidebar"><%= @content_for_sidebar %></div>
</div>
<div id="content"><%= @content_for_layout %></div>
<br class="clear"/>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,41 @@
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Mailr &raquo; Please Log In</title>
<link rel="stylesheet" type="text/css" href="/stylesheets/admin.css" />
<link rel="stylesheet" href="/stylesheets/mailr.css" type="text/css" media="screen" />
</head>
<body>
<div id="login" <%= 'class="login_error"' if @flash['error'] %>>
<h1>Mailr</h1>
<% if @flash['error'] %>
<div id="SystemError"><%= @flash['error'] %></div>
<% elsif @flash['status'] %>
<div id="SystemStatus"><%= @flash['status'] %></div>
<% end %>
<form action="<%=url_for(:controller => 'login', :action => 'authenticate')%>" method="post">
<table class="form_layout">
<tr>
<th><label for="<%=_('Email')%>"><%=_('Email')%>:</label></th>
<td><%= text_field "login_user", "email" %></td>
</tr>
<tr>
<th><label for="<%=_('Password')%>"><%=_('Password')%>:</label></th>
<td><%= password_field "login_user", "password" %></td>
</tr>
<tr>
<td colspan="2" class="form_actions">
<input type="submit" name="submit" value="<%= _('Log In') %>"/>
</td>
</tr>
</table>
</form>
<b class='cn tl'></b>
<b class='cn tr'></b>
<b class='cn bl'></b>
<b class='cn br'></b>
</div>
</body>
</html>

View file

@ -0,0 +1,68 @@
require 'cdfutils'
require_association 'contact_group'
class Contact < ActiveRecord::Base
has_and_belongs_to_many :groups, :class_name => "ContactGroup", :join_table => "contact_contact_groups", :association_foreign_key => "contact_group_id", :foreign_key => "contact_id"
# Finder methods follow
def Contact.find_by_user(user_id)
find(:all, :conditions => ["customer_id = ?", user_id], :order => "fname asc", :limit => 10)
end
def Contact.find_by_user_email(user_id, email)
find(:first, :conditions => ["customer_id = #{user_id} and email = ?", email])
end
def Contact.find_by_group_user(user_id, grp_id)
result = Array.new
find(:all, :conditions => ["customer_id = ?", user_id], :order => "fname asc").each { |c|
begin
c.groups.find(grp_id)
result << c
rescue ActiveRecord::RecordNotFound
end
}
result
end
def Contact.find_by_user_letter(user_id, letter)
find_by_sql("select * from contacts where customer_id=#{user_id} and substr(UPPER(fname),1,1) = '#{letter}' order by fname")
end
def full_name
"#{fname}&nbsp;#{lname}"
end
def show_name
"#{fname} #{lname}"
end
def full_address
"#{fname} #{lname}<#{email}>"
end
protected
def validate
errors.add 'fname', _('Please enter your first name (2 to 20 characters).') unless self.fname =~ /^.{2,20}$/i
errors.add 'lname', _('Please enter your surname (2 to 20 characters).') unless self.lname =~ /^.{2,20}$/i
# Contact e-mail cannot be changed
unless self.new_record?
old_record = Contact.find(self.id)
errors.add 'email', _('Contacts email cannot be changed.') unless old_record.email == self.email
end
end
def validate_on_create
# Contact e-mail cannot be changed, so we only need to validate it on create
errors.add 'email', _('Please enter a valid email address.') unless valid_email?(self.email)
# Already existing e-mail in contacts for this user is not allowed
if self.new_record?
if Contact.find_first("email = '#{email}' and customer_id = #{customer_id}")
errors.add('email', _('An account for your email address already exists.'))
end
end
end
end

View file

@ -0,0 +1,79 @@
<h1><%=_('Edit/Create contact')%></h1>
<div id="header">
<ul id="primary">
<li><%=link_folders%></li>
<li><%=link_send_mail%></li>
<li><%=link_mail_prefs%></li>
<li><%=link_mail_filters%></li>
<li><span><%= _('Contacts') %></span>
<ul id="secondary">
<li><%=link_contact_list%></li>
<% if ret = @session["return_to"] %>
<li><%=link_to(_('Back to message'), ret) %></li>
<% end %>
</ul>
</li>
</ul>
</div>
<div id="tab_main">
<div id="tab_content">
<%=
form_tag(
link_contact_save,
'method' => 'post',
'class' => 'two_columns'
)
%>
<%= form_input(:hidden_field, 'contact', 'id') %>
<%= form_input(:hidden_field, 'contact', 'customer_id') %>
<table>
<%= form_input(:text_field, 'contact', 'fname', _('First name'), 'class'=>'two_columns') %>
<%= form_input(:text_field, 'contact', 'lname', _('Last name'), 'class'=>'two_columns') %>
<%= form_input((@contact.new_record? ? :text_field : :read_only_field), 'contact', 'email', _('E-mail'), 'class'=>'two_columns')%>
</table>
<% for group in @contactgroups %>
<input id="groups[<%=group.id%>]" type="hidden" name="groups[<%=group.id%>]" value="<%=@groups[group.id]%>">
<% end %>
<% if not(@contactgroups.empty?) %>
<%=_('Contact belong to these groups')%>:
<table class="list">
<tr>
<%
end
col = 1
for group in @contactgroups %>
<th>
<input id="groups[<%=group.id%>]" type="checkbox" name="groups[<%=group.id%>]" value="<%=@groups[group.id]%>" onclick="toggleCheckbox(this)"
<%=@groups[group.id] == 1 ? " checked " : " " %> >
&nbsp;<%=group.name %>
</th>
<% if col%2 == 0 %>
</tr>
<tr>
<% end
col = col + 1 %>
<% end %>
<% if col%2 == 0 and not(@contactgroups.empty?) %>
<th>&nbsp;</th>
<% end %>
<% if not(@contactgroups.empty?) %>
</tr>
</table>
<% end %>
<table class="edit">
<tr>
<td colspan=2 class="buttonBar">
<input type="submit" name="paction" value="<%=_('Save')%>"/>
<input type="submit" name="paction" value="<%=_('Save and add another')%>"/>
</td>
</tr>
</table>
<%= end_form_tag %>
</div>
</div>

View file

@ -0,0 +1,26 @@
<h1><%=_('Add multiple contacts')%></h1>
<% if @flash["errors"] and not @flash["errors"].empty?%>
<%= _('Errors')%>
<ul>
<% @flash["errors"].each do |message| %>
<li><%= message %>
<% end %>
</ul>
<% end %>
<form action="<%=link_import_preview%>" enctype="multipart/form-data" method="post">
<%= radio_button("contact", "file_type", "1")%> <%= _('Comma-separated (CSV) file')%>
<%= radio_button("contact", "file_type", "2")%> <%= _('Tab-delimited text file')%>
<table>
<tr>
<th><label for="contact[data]"><%=_('Select file')%></label></th>
<td><input type="file" name="contact[data]"/></td>
</tr>
<tr>
<td colspan=2>
<input type="submit" value="<%=_('Import')%>"/>
<input type="button" value="<%= _('Back to contacts')%>" onclick="window.location='<%=link_contact_list%>'">
<input type="button" value="<%= _('Back to folders')%>" onclick="window.location='<%=link_main_index%>'">
</td>
</tr>
</table>
</form>

View file

@ -0,0 +1,11 @@
<script language="javascript">
<% for to in @tos %>
respondTo("<%=to.full_address%>", "<%=to.id%>");
<% end %>
<% for cc in @ccs %>
respondCC("<%=cc.full_address%>");
<% end %>
<% for bcc in @bccs %>
respondBCC("<%=bcc.full_address%>");
<% end %>
</script>

View file

@ -0,0 +1,43 @@
<h1><%= _('Contacts You Are About To Import')%></h1>
<% if @flash["errors"] and not @flash["errors"].empty?%>
<%= _('Errors')%>
<ul>
<% @flash["errors"].each do |message| %>
<li><%= message %>
<% end %>
</ul>
<% end %>
<form action="<%=link_contact_import%>" method="post">
<table class="list">
<tr>
<th>&nbsp;</th>
<th width="100px"><%= _('First name')%></th>
<th width="100px"><%= _('Last name')%></th>
<th><%= _('E-mail')%></th>
</tr>
<%
for i in 0...@contacts.length
contact = @contacts[i]
%>
<tr class="<%= alternator %>">
<td><%=i+1%></td>
<td><input type="text" name="contact[<%=i%>][fname]" value="<%=contact.fname%>" size="15" /></td>
<td><input type="text" name="contact[<%=i%>][lname]" value="<%=contact.lname%>" size="15" /></td>
<td><input type="text" name="contact[<%=i%>][email]" value="<%=contact.email%>" size="45" /></td>
</tr>
<% end %>
<tr>
<td colspan=4 class="buttonBar">
<input type="submit" value="<%= _('Import')%>">
<input type="button" value="<%= _('Choose another file')%>" onclick="window.location='<%=link_contact_add_multiple%>'">
<input type="button" value="<%= _('Back to contacts')%>" onclick="window.location='<%=link_contact_list%>'">
<input type="button" value="<%= _('Back to folders')%>" onclick="window.location='<%=link_main_index%>'">
</td>
</tr>
</table>
</form>

View file

@ -0,0 +1,114 @@
<h1><%= _('Contacts')%></h1>
<div id="header">
<ul id="primary">
<li><%=link_folders%></li>
<li><%=link_send_mail%></li>
<li><%=link_mail_prefs%></li>
<li><%=link_mail_filters%></li>
<li><span><%= _('Contacts') %></span>
<ul id="secondary">
<li><%=link_contact_add_one%></li>
<li><%=link_contact_add_multiple%></li>
<% if ret = @session["return_to"] %>
<li><%=link_to(_('Back to message'), ret) %></li>
<% end %>
</ul>
</li>
</ul>
</div>
<div id="tab_main">
<div id="tab_content">
<% if @flash["alert"] %><ul><li><%= @flash["alert"] %></li></ul><% end %>
<form action="<%=link_contact_choose%>?mode=<%=@mode%>" method="post">
<input type="hidden" name="mode" value="<%=@mode%>"/>
<% if @group_id and not @group_id.nil? %>
<input type="hidden" name="group_id" value="<%=@group_id%>"/>
<% end %>
<table class="list">
<tr>
<td colspan="4" id="alphaListHeader">
<%
letters = CDF::CONFIG[:contact_letters]
for letterIndex in 0...letters.size
letter = letters[letterIndex] %>
<%= link_to(letter, :controller=>"contact", :action=>"listLetter", :id=>letterIndex, :params=>{"mode"=>@mode, "group_id"=>(@group_id ? @group_id : nil)}) %>
<% end %>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<%= link_to(_('Show all'), :controller=>"contact", :action=>"list", :id=>(@group_id ? @group_id : nil), :params=>{"mode"=>@mode})%>
</td>
</tr>
<tr>
<td colspan="3"><%= @contact_pages.basic_html(self, 2, false, {"mode"=>@mode}) %></td>
</tr>
<% if @mode == "choose" %>
<tr>
<th><%= _('To&nbsp;CC&nbsp;BCC')%></th>
<th><%= _('Name')%></th>
<th><%= _('E-mail')%></th>
</tr>
<% for contact in @contacts %>
<tr class="<%= alternator %>">
<td><input type="checkbox" name="contacts_to[<%=contact.id%>]" value="1"/>
<input type="checkbox" name="contacts_cc[<%=contact.id%>]" value="1"/>
<input type="checkbox" name="contacts_bcc[<%=contact.id%>]" value="1"/></td>
<td><%=contact.full_name%></td>
<td><%=contact.email%></td>
</tr>
<% end %>
<tr class="rowsep"><td colspan="3"><%=_('Groups')%>:</td></tr>
<% for group in @contactgroups %>
<tr class="<%= alternator %>">
<td><input type="checkbox" name="groups_to[<%=group.id%>]" value="1"/>
<input type="checkbox" name="groups_cc[<%=group.id%>]" value="1"/>
<input type="checkbox" name="groups_bcc[<%=group.id%>]" value="1"/></td>
<td><%=group.name%></td>
<td>&nbsp;</td>
</tr>
<% end %>
<tr>
<td colspan=3 class="buttonBar">
<input type="submit" value="<%= _('choose')%>">
<input type="button" value="<%= _('cancel')%>" onclick="javascript:window.close();">
</td>
</tr>
<% elsif @mode == "groups"%>
<tr>
<th></th>
<th width="200px"><%= _('Name')%></th>
<th><%= _('E-mail')%></th>
</tr>
<% for contact in @contacts %>
<input type="hidden" id="contacts_for_group[<%=contact.id%>]" name="contacts_for_group[<%=contact.id%>]" value="<%=@contacts_for_group[contact.id]%>" >
<tr class="<%= alternator %>">
<td><input type="checkbox" id="contacts_for_group[<%=contact.id%>]" name="contacts_for_group[<%=contact.id%>]"
value="<%=@contacts_for_group[contact.id]%>" onclick="toggleCheckbox(this)"
<%=@contacts_for_group[contact.id] == 1 ? " checked " : " " %> ></td>
<td><%=contact.full_name%></td>
<td><%=contact.email%></td>
</tr>
<% end %>
<tr>
<td colspan=2 class="buttonBar">
<input type="submit" value="<%= _('Save')%>">
<input type="button" value="<%= _('Back to groups')%>" onclick="window.location='<%=link_contact_group_list%>'">
</td>
</tr>
<% else %>
<tr>
<th width="200px"><%= _('Name')%></th>
<th><%= _('E-mail')%></th>
<th>&nbsp;</th>
</tr>
<% for contact in @contacts %>
<tr class="<%= alternator %>">
<td><%= link_to(contact.full_name, :controller=>"/contacts/contact", :action => "edit", :id => contact.id ) %></td>
<td><%= link_to( contact.email, :controller => "/webmail", :action => "compose", :params => { "mail[to]" => contact.email } ) %></td>
<td><%= link_to(_('delete'), {:controller=>'/contacts/contact', :action=>'delete', :id=>contact.id}, {:confirm=>sprintf(_('DELETE CONTACT?\r\n\Name - %s\r\nE-mail - %s'), contact.show_name, contact.email)})%></td>
</tr>
<% end %>
<% end %>
</table>
</form>
</div>
</div>

View file

@ -0,0 +1,398 @@
class Contacts::ContactController < ApplicationController
uses_component_template_root
model :customer
model :contact
model :contact_group
helper :pagination
layout :select_layout
def index
redirect_to(:action =>"list")
end
def list
@contact_pages = Paginator.new(self, Contact.count("customer_id = #{logged_user}"), CDF::CONFIG[:contacts_per_page], @params['page'])
@contacts = Contact.find(:all, :conditions=>["customer_id = #{logged_user}"], :order=>['fname'], :limit=>CDF::CONFIG[:contacts_per_page], :offset=>@contact_pages.current.offset)
if @params["mode"] == "groups"
if @params["id"] and not @params["id"].nil? and not @params["id"] == ''
@group_id = @params["id"].to_i
@contacts_for_group = Hash.new
for contact in @contacts
@contacts_for_group[contact.id] = 0 # initialize
for gr in contact.groups
if gr.contact_group_id.to_i == @group_id
@contacts_for_group[contact.id] = 1 # checked
end
end
end
end
end
end
def listLetter
letters = CDF::CONFIG[:contact_letters]
@contact_pages = Paginator.new(self, Contact.count(
["customer_id = %s and substr(UPPER(fname),1,1) = '%s'", logged_user, letters[@params['id'].to_i]]), CDF::CONFIG[:contacts_per_page], @params['page'])
@contacts = Contact.find(:all, :conditions=>["customer_id = %s and substr(UPPER(fname),1,1) = '%s'", logged_user, letters[@params['id'].to_i]],
:order=>['fname'], :limit=>CDF::CONFIG[:contacts_per_page], :offset=>@contact_pages.current.offset)
if @params["mode"] == "groups"
if @params["group_id"] and not @params["group_id"].nil? and not @params["group_id"] == ''
@group_id = @params["group_id"].to_i
@contacts_for_group = Hash.new
for contact in @contacts
@contacts_for_group[contact.id] = 0 # initialize
for gr in contact.groups
if gr.contact_group_id.to_i == @group_id
@contacts_for_group[contact.id] = 1 # checked
end
end
end
end
end
render :action => "list"
end
def add
@contact = Contact.new
@contact.customer_id = logged_user
# load related lists
loadLists
# Init groups: because of checkbox
# Set all to 0 => unchecked
@groups = Hash.new
@contactgroups.each {|g|
@groups[g.id] = 0
}
end
def add_multiple
@contact = Contact.new
@contact["file_type"] = "1"
end
def add_from_mail
cstr = @params['cstr']
retmsg = @params['retmsg']
@session["return_to"] = url_for(:controller=>'/webmail/webmail',
:action=>'folders',
:msg_id=>retmsg)
# parse string
if i = cstr.index("<")
name, email = cstr.slice(0, i), cstr.slice((i+1)..(cstr.strip().index(">")-1))
fname = name.split().first
lname = name.split().last if name.split().size() > 1
else
fname, lname, email = "", "", cstr
end
if @contact = Contact.find_by_user_email(logged_user, email)
# load related lists
loadLists
@contact.fname, @contact.lname = fname, lname
# groups = @contact.groups
@groups = Hash.new
@contactgroups.each {|g|
groupSelected = false
@contact.groups.each {|gr|
if gr.contact_group_id.to_i == g.id.to_i
groupSelected = true
break
end
}
if groupSelected
@groups[g.id] = 1 # checked
else
@groups[g.id] = 0 # unchecked
end
}
else
@contact = Contact.new("fname"=>fname, "lname" => lname, "email" => email)
@contact.customer_id = logged_user
# load related lists
loadLists
# Init groups: because of checkbox
# Set all to 0 => unchecked
@groups = Hash.new
@contactgroups.each {|g|
@groups[g.id] = 0
}
end
render :action => "add"
end
def import_preview
file = @params["contact"]["data"]
flash["errors"] = Array.new
if file.size == 0
flash["errors"] << _('You haven\'t selected file or the file is empty')
@contact = Contact.new
@contact["file_type"] = @params["contact"]["file_type"]
render :action => "add_multiple"
end
file_type = @params["contact"]["file_type"]
if file_type.nil? or file_type == '1'
separator = ','
else
separator = /\t/
end
@contacts = Array.new
emails = Array.new
file.each {|line|
cdata = line.strip.chomp.split(separator)
cont = Contact.new
cont.fname = cdata[0].to_s.strip.chomp
cont.lname = cdata[1].to_s.strip.chomp
cont.email = cdata[2].to_s.strip.chomp
# Check for duplicate emails in the file
if emails.include?(cont.email)
flash["errors"] << sprintf(_('Contact %'), file.lineno.to_s) + ": " + _('The e-mail duplicates the e-mail of another record!')
else
emails << cont.email
end
@contacts << cont
}
end
def import
contacts_count = @params["contact"].length
contacts_to_import = @params["contact"]
@contacts = Array.new
emails = Array.new
flash["errors"] = Array.new
for i in 0...contacts_count
contact = Contact.new
contact.customer_id = logged_user
contact.fname = contacts_to_import[i.to_s]["fname"]
contact.lname = contacts_to_import[i.to_s]["lname"]
contact.email = contacts_to_import[i.to_s]["email"]
begin
# Check for duplicate emails in the submitted data
if emails.include?(contact.email)
flash["errors"] << sprintf(_('Contact %'), (i+1).to_s) + ": " + _('The e-mail duplicates the e-mail of another record!')
else
emails << contact.email
end
# Check if contact is valid
contact.valid?
rescue CDF::ValidationError => e
if not contact.errors.empty?
["fname", "lname", "email"].each do |attr|
attr_errors = contact.errors.on(attr)
attr_errors = [attr_errors] unless attr_errors.nil? or attr_errors.is_a? Array
if not attr_errors.nil?
attr_errors.each do |msg|
flash["errors"] << l(:contact_addmultiple_errorforcontact, (i+1).to_s) + ": " + l(msg)
end
end
end
end
end # rescue
@contacts << contact
end # for
# If there are validation errors - display them
if not flash["errors"].nil? and not flash["errors"].empty?
render :action => "import_preview"
else
# save
begin
for contact in @contacts
Contact.create(contact.attributes)
end
# Set message for successful import
flash["alert"] = Array.new
flash["alert"] << l(:contact_addmultiple_success, @contacts.length.to_s)
keep_flash()
redirect_to(:action=>"list")
rescue Exception => exc
flash["errors"] << exc
render :action => "import_preview"
end
end
end
def choose
if @params["mode"] == "groups"
save_groups
end
@tos, @ccs, @bccs = Array.new, Array.new, Array.new
@params["contacts_to"].each{ |id,value| @tos << Contact.find(id) if value == "1" } if @params["contacts_to"]
@params["contacts_cc"].each{ |id,value| @ccs << Contact.find(id) if value == "1" } if @params["contacts_cc"]
@params["contacts_bcc"].each{ |id,value| @bccs << Contact.find(id) if value == "1" } if @params["contacts_bcc"]
@params["groups_to"].each{ |id,value|
ContactGroup.find(id).contacts.each {|c| @tos << c} if value == "1" } if @params["groups_to"]
@params["groups_cc"].each{ |id,value|
ContactGroup.find(id).contacts.each {|c| @ccs << c} if value == "1" } if @params["groups_cc"]
@params["groups_bcc"].each{ |id,value|
ContactGroup.find(id).contacts.each {|c| @bccs << c} if value == "1" } if @params["groups_bcc"]
end
def save_groups
contacts_for_group = @params["contacts_for_group"]
group_id = @params["group_id"]
contact_group = ContactGroup.find(group_id)
contacts_for_group.each { |contact_id,value|
contact = Contact.find(contact_id)
if value == "1" and not contact_group.contacts.include?(contact)
contact_group.contacts << contact
end
if value == "0" and contact_group.contacts.include?(contact)
contact_group.contacts.delete(contact)
end
}
redirect_to(:action=>"list", :id=>group_id, :params=>{"mode"=>@params["mode"]})
end
def edit
@contact = Contact.find(@params["id"])
# load related lists
loadLists
# groups = @contact.groups
@groups = Hash.new
@contactgroups.each {|g|
groupSelected = false
@contact.groups.each {|gr|
if gr.contact_group_id.to_i == g.id.to_i
groupSelected = true
break
end
}
if groupSelected
@groups[g.id] = 1 # checked
else
@groups[g.id] = 0 # unchecked
end
}
render :action => "add"
end
# Insert or update
def save
logger.info("BEGIN")
if @params["contact"]["id"] == ""
# New contact
@contact = Contact.create(@params["contact"])
else
# Edit existing
@contact = Contact.find(@params["contact"]["id"])
@contact.attributes = @params["contact"]
end
@contactgroups = ContactGroup.find_by_user(logged_user)
# Groups displayed
groups = @params['groups']
tempGroups = Array.new
tempGroups.concat(@contact.groups)
@contactgroups.each { |cgroup|
includesCGroup = false
tempGroups.each {|gr|
if gr.contact_group_id.to_i == cgroup.id.to_i
includesCGroup = true
break
end
}
if groups["#{cgroup.id}"] == "1" and not includesCGroup
@contact.groups << cgroup
end
if groups["#{cgroup.id}"] == "0" and includesCGroup
@contact.groups.delete(cgroup)
end
}
if @contact.save
if @params["paction"] == _('Save')
redirect_to :controller => "/contacts/contact", :action =>"list"
else
redirect_to :controller => "/contacts/contact", :action =>"add"
end
else
loadLists
@groups = Hash.new
@contactgroups.each {|g|
if @contact.groups.include?(g)
@groups[g.id] = 1
else
@groups[g.id] = 0
end
}
render :action => "add"
end
end
def delete
Contact.destroy(@params['id'])
redirect_to(:action=>'list')
end
protected
def secure_user?() true end
def additional_scripts()
add_s = ''
if action_name == "choose"
add_s<<'<script type="text/javascript" src="/javascripts/global.js"></script>'
add_s<<'<script type="text/javascript" src="/javascripts/contact_choose.js"></script>'
end
add_s
end
def onload_function()
if action_name == "choose"
"javascript:respondToCaller();"
else
""
end
end
private
def select_layout
if @params["mode"] == "choose"
@mode = "choose"
@contactgroups = ContactGroup.find_by_user(logged_user)
'chooser'
elsif @params["mode"] == "groups"
@mode = "groups"
'public'
else
@mode = "normal"
'public'
end
end
def loadLists
if @contactgroups.nil?
@contactgroups = ContactGroup.find_by_user(logged_user)
end
end
end

View file

@ -0,0 +1,25 @@
class ContactGroup < ActiveRecord::Base
has_and_belongs_to_many :contacts, :class_name => "Contact", :join_table => "contact_contact_groups", :association_foreign_key => "contact_id", :foreign_key => "contact_group_id"
def ContactGroup.find_by_user(user_id)
find_by_sql("select * from contact_groups where customer_id = #{user_id} order by name asc")
end
protected
def validate
errors.add('name', :contactgroup_name_invalid) unless self.name =~ /^.{1,50}$/i
end
def validate_on_create
if ContactGroup.find_first(["name = '#{name}' and customer_id = #{user_id}"])
errors.add("name", _('Please enter group name (1 to 50 characters)'))
end
end
def validate_on_update
if ContactGroup.find_first(["name = '#{name}' and customer_id = #{user_id} and id <> #{id}"])
errors.add("name", _('You already have contact group with this name'))
end
end
end

View file

@ -0,0 +1,24 @@
<h1><%=_('Edit/Create Contact Group')%></h1>
<%=
form_tag(
link_save,
'method' => 'post',
'class' => 'two_columns'
)
%>
<%= form_input(:hidden_field, 'contactgroup', 'id') %>
<%= form_input(:hidden_field, 'contactgroup', 'customer_id') %>
<table>
<%= form_input(:text_field, 'contactgroup', 'name', _('Name'), 'class'=>'two_columns') %>
</table>
<table>
<tr>
<td colspan=2 class="buttonBar">
<input type="submit" name="Save" value="<%=_('Save')%>"/>
<input type="button" value="<%=_('Back to groups')%>" onclick="window.location='<%=link_list%>'"/>
</td>
</tr>
</table>
<%= end_form_tag %>

View file

@ -0,0 +1,26 @@
<h1><%=_('Contact Groups')%></h1>
<form action="/contact_group/add" method="post">
<%= hidden_field "contactgroup", "user_id" %>
<table class="list">
<tr>
<th><%=_('Name')%></th>
<th colspan=3>&nbsp;</th>
</tr>
<%
for contactgroup in @contactgroups %>
<tr class="even">
<td><%= contactgroup.name %></td>
<td><%= link_to(_('members'), :controller=>'contact', :action=>'list', :id=>contactgroup.id, :params=>{"mode"=>"groups"}) %></td>
<td><%= link_to(_('edit'), :controller=>'/contacts/contact_group', :action=>'edit', :id=>contactgroup.id) %></td>
<td><%= link_to(_('delete'), {:controller=>'/contacts/contact_group', :action=>'delete', :id=>contactgroup.id}, {:confirm=>sprintf(_('DELETE CONTACT GROUP \'%s\'?'), contactgroup.name)})%></td>
</tr>
<% end %>
<tr>
<td colspan=2 class="buttonBar">
<input type="submit" value="<%=_('Add Contact Group')%>"/>
<input type="button" value="<%=_('Back to folders')%>" onclick="window.location='/webmail/folders'">
</td>
</tr>
</table>
</form>

View file

@ -0,0 +1,60 @@
class Contacts::ContactGroupController < ApplicationController
uses_component_template_root
model :contact_group
layout 'public'
def index
redirect_to(:action=>"list")
end
def list
@contactgroup = ContactGroup.new
@contactgroup.customer_id = logged_user
@contactgroups = ContactGroup.find_by_user(logged_user)
end
def add
@contactgroup = ContactGroup.new
@contactgroup.customer_id = logged_user
render("/contact_group/edit")
end
def delete
contactgroup = ContactGroup.find(@params["id"])
contactgroup.destroy
redirect_to(:action=>"list")
end
def edit
@contactgroup = ContactGroup.find(@params["id"])
end
def save
begin
if @params["contactgroup"]["id"].nil? or @params["contactgroup"]["id"] == ""
# New contactgroup
@contactgroup = ContactGroup.create(@params["contactgroup"])
else
# Edit existing
@contactgroup = ContactGroup.find(@params["contactgroup"]["id"])
@contactgroup.attributes = @params["contactgroup"]
end
if @contactgroup.save
redirect_to(:action=>"list")
else
render "/contact_group/edit"
end
rescue CDF::ValidationError => e
logger.info("RESCUE")
@contactgroup = e.entity
render("/contact_group/edit")
end
end
protected
def secure_user?() true end
end

View file

@ -0,0 +1,4 @@
module ContactGroupHelper
def link_save() "/contact_group/save" end
def link_list() "/contact_group/list" end
end

View file

@ -0,0 +1,40 @@
module Contacts::ContactHelper
def link_import_preview() "/contacts/contact/import_preview" end
def link_main_index() "/webmail/webmail/folders" end
def link_contact_save() "/contacts/contact/save" end
def link_contact_import() "/contacts/contact/import" end
def link_contact_choose() "/contacts/contact/choose" end
def link_contact_list
link_to(_('List'), :controller => "/contacts/contact", :action => "list")
end
def link_contact_add_one
link_to(_('Add one contact'), :controller => "/contacts/contact", :action => "add")
end
def link_contact_add_multiple
link_to(_('Add multiple'), :controller => "/contacts/contact", :action => "add_multiple")
end
def link_contact_group_list
link_to(_('Groups'), :controller => "/contacts/contact_group", :action => "list")
end
def link_folders
link_to(_('Folders'), :controller=>"/webmail/webmail", :action=>"messages")
end
def link_send_mail
link_to(_('Compose'), :controller=>"/webmail/webmail", :action=>"compose")
end
def link_mail_prefs
link_to(_('Preferences'), :controller=>"/webmail/webmail", :action=>"prefs")
end
def link_mail_filters
link_to(_('Filters'), :controller=>"/webmail/webmail", :action=>"filters")
end
end

View file

@ -0,0 +1,32 @@
require_dependency 'maildropserializator'
class Customer < ActiveRecord::Base
include MaildropSerializator
has_many :filters, :order => "order_num"
has_one :mail_pref
attr_accessor :password
def mail_temporary_path
"#{CDF::CONFIG[:mail_temp_path]}/#{self.email}"
end
def friendlly_local_email
encode_email("#{self.fname} #{self.lname}", check_for_domain(email))
end
def mail_filter_path
"#{CDF::CONFIG[:mail_filters_path]}/#{self.email}"
end
def local_email
self.email
end
def check_for_domain(email)
if email and !email.nil? and !email.include?("@")
email + "@" + CDF::CONFIG[:send_from_domain]
else
email
end
end
end

View file

@ -0,0 +1,18 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<%= @htmllang %>" lang="<%= @htmllang %>">
<head>
<title><%=@title%></title>
<meta http-equiv="content-type" content="text/html; charset=<%= @charset %>" />
<script type="text/javascript" src="/javascripts/global.js"></script>
<link rel="stylesheet" href="/stylesheets/admin.css" type="text/css" media="screen" />
<link rel="stylesheet" href="/stylesheets/tabs.css" type="text/css" media="screen" />
<link rel="stylesheet" href="/stylesheets/mailr.css" type="text/css" media="screen" />
<%=@additional_scripts%>
</head>
<body id="bodyID" onload="<%=@onload_function%>">
<%= @content_for_layout %>
</body>
</html>

View file

@ -0,0 +1,26 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<%= @htmllang %>" lang="<%= @htmllang %>">
<head>
<title><%=_('Mailr')%></title>
<meta http-equiv="content-type" content="text/html; charset=<%= @charset %>" />
<link rel="stylesheet" href="/stylesheets/admin.css" type="text/css" media="screen" />
<link rel="stylesheet" href="/stylesheets/tabs.css" type="text/css" media="screen" />
<link rel="stylesheet" href="/stylesheets/mailr.css" type="text/css" media="screen" />
<script type="text/javascript" src="/javascripts/global.js"></script>
<script type="text/javascript" src="/javascripts/prototype.js"></script>
<%=(@content_for_scripts ? @content_for_scripts : @additional_scripts )%>
</head>
<body id="bodyID" onload="<%=@onload_function%>">
<div id="wholepage">
<div id="container">
<div id="logout"><%= link_to _('[Logout]'), :controller=>'/login', :action=>'logout' %></div>
<div id="sidebar_outer">
<div id="sidebar"><%= @content_for_sidebar %></div>
</div>
<div id="content"><%= @content_for_layout %></div>
<br class="clear"/>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,8 @@
class BouncedMail < ActiveRecord::Base
belongs_to :customer
belongs_to :contact
def BouncedMail.find_by_customer_contact(cust_id, contact_id)
find_all(["customer_id = ? and contact_id = ?", cust_id, cotact_id], ["msg_date desc"])
end
end

View file

@ -0,0 +1,303 @@
require 'tmail'
require 'net/smtp'
require 'mail_transform'
class CDF::Mail
include ActionMailer::Quoting
def initialize(senderTempLocation)
@attachments = Array.new
@sender_temp_location = senderTempLocation
@to_contacts = Array.new
end
def customer_id() @customer_id end
def customer_id=(arg) @customer_id = arg end
def from() @from end
def from=(arg) @from = arg end
def to() @to end
def to=(arg) @to = arg end
def to_contacts() @to_contacts end
def to_contacts=(arg) @to_contacts = arg end
def toc=(arg)
@to_contacts = Array.new
arg.split(",").each { |token| @to_contacts << token.to_i unless token == "" or token.strip() == "undefined"} unless arg.nil? or arg == "undefined"
end
def toc
ret = String.new
@to_contacts.each { |contact|
ret << "," unless ret == ""
if contact.kind_of?(Integer)
ret << contact.to_s unless contact.nil? or contact == 0
else
ret << contact.id.to_s unless contact.nil? or contact.id.nil?
end
}
ret
end
def bcc() @bcc end
def bcc=(arg) @bcc = arg end
def cc() @cc end
def cc=(arg) @cc = arg end
def subject() @subject end
def subject=(arg) @subject = arg end
def attachments
@attachments
end
def add_attachment(attachment)
@attachments << attachment
end
def multipart?
@attachments && @attachments.size > 0
end
def delete_attachment(att_filename)
@attachments.each { |att| att.delete_temp_data() if arr.filename == att_filename }
@attachments.delete_if() { |att| att.filename == att_filename }
end
def delete_attachments()
@attachments.each { |att| att.delete_temp_data() }
@attachments = Array.new
end
def body() @body end
def body=(arg) @body = arg end
def content_type() @content_type end
def content_type=(arg) @content_type = arg end
def temp_location() @sender_temp_location end
def send_mail(db_msg_id = 0)
m = TMail::Mail.new
m.from, m.body = self.from, self.body
m.date = Time.now
m.subject, = quote_any_if_necessary("UTF-8", self.subject)
m.to = decode_addresses(self.to)
m.cc, m.bcc = decode_addresses(self.cc), decode_addresses(self.bcc)
if multipart?
m.set_content_type("multipart/mixed")
p = TMail::Mail.new(TMail::StringPort.new(""))
if @content_type.include?("text/plain") # here maybe we should encode in 7bit??!!
prepare_text(p, self.content_type, self.body)
elsif self.content_type.include?("text/html")
prepare_html(p, self.content_type, self.body)
elsif self.content_type.include?("multipart")
prepare_alternative(p, self.body)
end
m.parts << p
else
if @content_type.include?("text/plain") # here maybe we should encode in 7bit??!!
prepare_text(m, self.content_type, self.body)
elsif self.content_type.include?("text/html")
prepare_html(m, self.content_type, self.body)
elsif self.content_type.include?("multipart")
prepare_alternative(m, self.body)
end
end
# attachments
@attachments.each { |a|
m.parts << a.encoded
}
encmail = m.encoded
RAILS_DEFAULT_LOGGER.debug("Sending message \n #{encmail}")
Net::SMTP.start(ActionMailer::Base.server_settings[:address], ActionMailer::Base.server_settings[:port],
ActionMailer::Base.server_settings[:domain], ActionMailer::Base.server_settings[:user_name],
ActionMailer::Base.server_settings[:password], ActionMailer::Base.server_settings[:authentication]) do |smtp|
smtp.sendmail(encmail, m.from, m.destinations)
end
return encmail
end
def forward(tmail, fb)
decoded_subject = mime_encoded?(tmail.subject) ? mime_decode(tmail.subject) : tmail.subject
self.subject = "[Fwd: #{decoded_subject}]"
attachment = CDF::Attachment.new(self)
attachment.body(tmail, fb)
end
def reply(tmail, fb, type)
decoded_subject = mime_encoded?(tmail.subject) ? mime_decode(tmail.subject) : tmail.subject
self.subject = "[Re: #{decoded_subject}]"
tm = tmail.setup_reply(tmail)
self.to = tm.to
footer = ""
msg_id = ""
mt = MailTransform.new
self.body = mt.get_body(tmail, type)
end
private
def delimeter
if self.content_type == "text/plain"
"\n"
else
"<br/>"
end
end
def text2html(str) CGI.escapeHTML(str).gsub("\n", "<br/>") end
def html2text(txt)
clear_html(txt)
end
def prepare_text(msg, ctype, bdy)
msg.set_content_type(ctype, nil, {"charset"=>"utf-8"})
msg.transfer_encoding = "8bit"
msg.body = bdy
end
def prepare_html(msg, ctype, bdy)
msg.set_content_type(ctype, nil, {"charset"=>"utf8"})
msg.transfer_encoding = "8bit"
msg.body = bdy
end
def prepare_alternative(msg, bdy)
bound = ::TMail.new_boundary
msg.set_content_type("multipart/alternative", nil, {"charset"=>"utf8", "boundary"=>bound})
msg.transfer_encoding = "8bit"
ptext = TMail::Mail.new(TMail::StringPort.new(""))
phtml = TMail::Mail.new(TMail::StringPort.new(""))
prepare_text(ptext, "text/plain", html2text(bdy))
prepare_html(phtml, "text/html", bdy)
msg.parts << ptext
msg.parts << phtml
end
def decode_addresses(str)
ret = String.new
str.split(",").each { |addr|
if addr.slice(0,4) == "Grp+"
grp_id = addr.scan(/Grp\+([0-9]*):(.*)/)[0][0]
ContactGroup.find(:first, :conditions=>['customer_id = ? and id = ?', @customer_id, grp_id]).contacts.each { |contact|
ret << "," if not(ret == "")
@to_contacts << contact unless contact.nil?
ret << contact.full_address
ad, = quote_any_address_if_necessary(CDF::CONFIG[:mail_charset], contact.full_address)
ret << ad
}
else
ret << "," if not(ret == "")
ad, = quote_any_address_if_necessary(CDF::CONFIG[:mail_charset], addr) if not(addr.nil? or addr == "")
ret << ad if not(addr.nil? or addr == "")
end
} unless str.nil? or str.strip() == ""
ret
end
end
class CDF::Attachment
def initialize(arg)
@mail = arg
@mail.add_attachment(self)
@index = @mail.attachments.size - 1
end
def filename=(arg)
@filename = arg.tr('\\/:*?"\'<>|', '__________')
end
def filename() @filename end
def temp_filename=(arg) @temp_filename = arg end
def temp_filename() @temp_filename end
def content_type=(arg) @content_type = arg end
def content_type() @content_type end
def delete_temp_data()
File.delete(self.temp_filename)
end
def file
File.open(self.temp_filename, "rb") { |fp| fp.read }
end
def file=(data)
return if data.size == 0
@content_type = data.content_type
self.filename = data.original_filename.scan(/[^\\]*$/).first
self.temp_filename = "#{@mail.temp_location}/#{@filename}"
check_store_path
data.rewind
File.open(@temp_filename, "wb") { |f| f.write(data.read) }
end
def body(data, fb)
@content_type = "message/rfc822"
filename = data.content_type['filename']
self.filename = filename.nil? ? (mime_encoded?(data.subject) ? mime_decode(data.subject) : data.subject) : filename
self.temp_filename = "#{@mail.temp_location}/#{@filename}"
check_store_path
File.open(@temp_filename, "wb") { |f| f.write(fb) }
end
def check_store_path()
path = ""
"#{@mail.temp_location}".split(File::SEPARATOR).each { |p|
path << p
begin
Dir.mkdir(path)
rescue
end
path << File::SEPARATOR
}
end
def encoded
p = TMail::Mail.new(TMail::StringPort.new(""))
data = self.file
p.body = data
if @content_type.include?("text/plain") # here maybe we should encode in 7bit??!!
p.set_content_type(@content_type, nil, {"charset"=>"utf-8"})
p.transfer_encoding = "8bit"
elsif @content_type.include?("text/html")
p.set_content_type(@content_type, nil, {"charset"=>"utf8"})
p.transfer_encoding = "8bit"
elsif @content_type.include?("rfc822")
p.set_content_type(@content_type, nil, {"charset"=>"utf8"})
p.set_disposition("inline;")
p.transfer_encoding = "8bit"
else
p.set_content_type(@content_type, nil, {"name"=>@filename})
p.set_disposition("inline; filename=#{@filename}") unless @filename.nil?
p.set_disposition("inline;") if @filename.nil?
p.transfer_encoding='Base64'
p.body = TMail::Base64.folding_encode(data)
end
return p
end
end

View file

@ -0,0 +1,5 @@
require 'maildropserializator'
Customer.class_eval do
include MaildropSerializator
has_many :filters, :order => "order_num", :dependent => true
end

View file

@ -0,0 +1,2 @@
class Expression < ActiveRecord::Base
end

View file

@ -0,0 +1,3 @@
class Filter < ActiveRecord::Base
has_many :expressions
end

View file

@ -0,0 +1,38 @@
require 'mail2screen'
class ImapMessage < ActiveRecord::Base
include Mail2Screen
def set_folder(folder)
@folder = folder
end
def full_body
@folder.mailbox.imap.uid_fetch(uid, "BODY[]").first.attr["BODY[]"]
end
def from_addr=(fa)
self.from = fa.to_yaml
self.from_flat = short_address(fa)
end
def from_addr
begin
YAML::load(from)
rescue Object
from
end
end
def to_addr=(ta)
self.to = ta.to_yaml
self.to_flat = short_address(ta)
end
def to_addr
begin
YAML::load(to)
rescue Object
to
end
end
end

View file

@ -0,0 +1,497 @@
# Copyright (c) 2005, Benjamin Stiglitz
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Modifications (c) 2005 by littlegreen
#
require 'net/imap'
Net::IMAP.debug = true if CDF::CONFIG[:debug_imap]
class Net::IMAP
class PlainAuthenticator
def process(data)
return "\0#{@user}\0#{@password}"
end
private
def initialize(user, password)
@user = user
@password = password
end
end
add_authenticator('PLAIN', PlainAuthenticator)
class Address
def to_s
if(name)
"#{name} #{mailbox}@#{host}"
else
"#{mailbox}@#{host}"
end
end
end
end
class AuthenticationError < RuntimeError
end
class IMAPMailbox
attr_reader :connected
attr_accessor :selected_mailbox
cattr_accessor :logger
def initialize
@selected_mailbox = ''
@folders = {}
@connected = false
end
def connect(username, password)
unless @connected
use_ssl = CDF::CONFIG[:imap_use_ssl] ? true : false
port = CDF::CONFIG[:imap_port] || (use_ssl ? 993 : 143)
begin
@imap = Net::IMAP.new(CDF::CONFIG[:imap_server], port, use_ssl)
rescue Net::IMAP::ByeResponseError => bye
# make a timeout and retry
begin
System.sleep(CDF::CONFIG[:imap_bye_timeout_retry_seconds])
@imap = Net::IMAP.new(CDF::CONFIG[:imap_server], port, use_ssl)
rescue Error => ex
logger.error "Error on authentication!"
logger.error bye.backtrace.join("\n")
raise AuthenticationError.new
end
rescue Net::IMAP::NoResponseError => noresp
logger.error "Error on authentication!"
logger.error noresp.backtrace.join("\n")
raise AuthenticationError.new
rescue Net::IMAP::BadResponseError => bad
logger.error "Error on authentication!"
logger.error bad.backtrace.join("\n")
raise AuthenticationError.new
rescue Net::IMAP::ResponseError => resp
logger.error "Error on authentication!"
logger.error resp.backtrace.join("\n")
raise AuthenticationError.new
end
@username = username
begin
logger.error "IMAP authentication - #{CDF::CONFIG[:imap_auth]}."
if CDF::CONFIG[:imap_auth] == 'NOAUTH'
@imap.login(username, password)
else
@imap.authenticate(CDF::CONFIG[:imap_auth], username, password)
end
@connected = true
rescue Exception => ex
logger.error "Error on authentication!"
logger.error ex.backtrace.join("\n")
raise AuthenticationError.new
end
end
end
def imap
@imap
end
# Function chnage password works only if root has run imap_backend
# and users courier-authlib utility authtest - from courier-imap version 4.0.1
def change_password(username, password, new_password)
ret = ""
cin, cout, cerr = Open3.popen3("/usr/sbin/authtest #{username} #{password} #{new_password}")
ret << cerr.gets
if ret.include?("Password change succeeded.")
return true
else
logger.error "[!] Error on change password! - #{ret}"
return false
end
end
def disconnect
if @connected
@imap.logout
@imap.disconnect
@imap = nil
@connected = false
end
end
def [](mailboxname)
@last_folder = IMAPFolderList.new(self, @username)[mailboxname]
end
def folders
# reference just to stop GC
@folder_list ||= IMAPFolderList.new(self, @username)
@folder_list
end
def reload
@folder_list.reload if @folder_list
end
def create_folder(name)
begin
@imap.create(name)
reload
rescue Exception=>e
end
end
def delete_folder(name)
begin
@imap.delete(name)
reload
rescue Exception=>e
logger.error("Exception on delete #{name} folder #{e}")
end
end
def message_sent(message)
# ensure we have sent folder
begin
@imap.create(CDF::CONFIG[:mail_sent])
rescue Exception=>e
end
begin
@imap.append(CDF::CONFIG[:mail_sent], message)
folders[CDF::CONFIG[:mail_sent]].cached = false if folders[CDF::CONFIG[:mail_sent]]
rescue Exception=>e
logger.error("Error on append - #{e}")
end
end
def message_bulk(message)
# ensure we have sent folder
begin
@imap.create(CDF::CONFIG[:mail_bulk_sent])
rescue Exception=>e
end
begin
@imap.append(CDF::CONFIG[:mail_bulk_sent], message)
folders[CDF::CONFIG[:mail_sent]].cached = false if folders[CDF::CONFIG[:mail_bulk_sent]]
rescue Exception=>e
logger.error("Error on bulk - #{e}")
end
end
end
class IMAPFolderList
include Enumerable
cattr_accessor :logger
def initialize(mailbox, username)
@mailbox = mailbox
@folders = Hash.new
@username = username
end
def each
refresh if @folders.empty?
#@folders.each_value { |folder| yield folder }
# We want to allow sorted access; for now only (FIXME)
@folders.sort.each { |pair| yield pair.last }
end
def reload
refresh
end
def [](name)
refresh if @folders.empty?
@folders[name]
end
private
def refresh
@folders = {}
result = @mailbox.imap.list('', '*')
if result
result.each do |info|
@folders[info.name] = IMAPFolder.new(@mailbox, info.name, @username, info.attr, info.delim)
end
else
# if there are no folders subscribe to INBOX - this is on first use
@mailbox.imap.subscribe(CDF::CONFIG[:mail_inbox])
# try again to list them - we should find INBOX
@mailbox.imap.list('', '*').each do |info|
@folders[info.name] = IMAPFolder.new(@mailbox, info.name, @username, info.attr, info.delim)
end
end
@folders
end
end
class IMAPFolder
attr_reader :mailbox
attr_reader :name
attr_reader :username
attr_reader :delim
attr_reader :attribs
attr_writer :cached
attr_writer :mcached
cattr_accessor :logger
@@fetch_attr = ['ENVELOPE','BODYSTRUCTURE', 'FLAGS', 'UID', 'RFC822.SIZE']
def initialize(mailbox, name, username, attribs, delim)
@mailbox = mailbox
@name = name
@username = username
@messages = Array.new
@delim = delim
@attribs = attribs
@cached = false
@mcached = false
end
def activate
if(@mailbox.selected_mailbox != @name)
@mailbox.selected_mailbox = @name
@mailbox.imap.select(@name)
load_total_unseen if !@cached
end
end
# Just delete message without interaction with Trash folder
def delete(message)
uid = (message.kind_of?(Integer) ? message : message.uid)
@mailbox.imap.uid_store(uid, "+FLAGS", :Deleted)
@mailbox.imap.expunge
# Sync with trash cannot be made - new uid generated - so just delete message from current folder
ImapMessage.delete_all(["username = ? and folder_name = ? and uid = ?", @username, @name, uid])
@cached = false
end
# Deleted messages - move to trash folder
def delete_multiple(uids)
# ensure we have trash folder
begin
@mailbox.imap.create(CDF::CONFIG[:mail_trash])
rescue
end
move_multiple(uids, CDF::CONFIG[:mail_trash])
end
def copy(message, dst_folder)
uid = (message.kind_of?(Integer) ? message : message.uid)
activate
@mailbox.imap.uid_copy(uid, dst_folder)
@mailbox.folders[dst_folder].cached = false if @mailbox.folders[dst_folder]
@mailbox.folders[dst_folder].mcached = false if @mailbox.folders[dst_folder]
end
def copy_multiple(message_uids, dst_folder)
activate
@mailbox.imap.uid_copy(message_uids, dst_folder)
@mailbox.folders[dst_folder].cached = false if @mailbox.folders[dst_folder]
@mailbox.folders[dst_folder].mcached = false if @mailbox.folders[dst_folder]
end
def move(message, dst_folder)
uid = (message.kind_of?(Integer) ? message : message.uid)
activate
@mailbox.imap.uid_copy(uid, dst_folder)
@mailbox.imap.uid_store(uid, "+FLAGS", :Deleted)
@mailbox.folders[dst_folder].cached = false if @mailbox.folders[dst_folder]
@mailbox.folders[dst_folder].mcached = false if @mailbox.folders[dst_folder]
@mailbox.imap.expunge
ImapMessage.delete_all(["username = ? and folder_name = ? and uid = ? ", @username, @name, uid])
@cached = false
@mcached = false
end
def move_multiple(message_uids, dst_folder)
activate
@mailbox.imap.uid_copy(message_uids, dst_folder)
@mailbox.imap.uid_store(message_uids, "+FLAGS", :Deleted)
@mailbox.folders[dst_folder].cached = false if @mailbox.folders[dst_folder]
@mailbox.folders[dst_folder].mcached = false if @mailbox.folders[dst_folder]
@mailbox.imap.expunge
ImapMessage.delete_all(["username = ? and folder_name = ? and uid in ( ? )", @username, @name, message_uids])
@cached = false
@mcached = false
end
def mark_read(message_uid)
activate
cached = ImapMessage.find(:first, :conditions => ["username = ? and folder_name = ? and uid = ?", @username, @name, message_uid])
if cached.unread
cached.unread = false
cached.save
@mailbox.imap.select(@name)
@mailbox.imap.uid_store(message_uid, "+FLAGS", :Seen)
@unseen_messages = @unseen_messages - 1
end
end
def mark_unread(message_uid)
activate
cached = ImapMessage.find(:first, :conditions => ["username = ? and folder_name = ? and uid = ?", @username, @name, message_uid])
if !cached.unread
cached.unread = true
cached.save
@mailbox.imap.select(@name)
@mailbox.imap.uid_store(message_uid, "-FLAGS", :Seen)
@unseen_messages = @unseen_messages + 1
end
end
def expunge
activate
@mailbox.imap.expunge
end
def synchronize_cache
startSync = Time.now
activate
startUidFetch = Time.now
server_messages = @mailbox.imap.uid_fetch(1..-1, ['UID', 'FLAGS'])
startDbFetch = Time.now
cached_messages = ImapMessage.find(:all, :conditions => ["username = ? and folder_name = ?", @username, @name])
cached_unread_uids = Array.new
cached_read_uids = Array.new
uids_to_be_deleted = Array.new
cached_messages.each { |msg|
cached_unread_uids << msg.uid if msg.unread
cached_read_uids << msg.uid unless msg.unread
uids_to_be_deleted << msg.uid
}
uids_to_be_fetched = Array.new
server_msg_uids = Array.new
uids_unread = Array.new
uids_read = Array.new
server_messages.each { |server_msg|
uid, flags = server_msg.attr['UID'], server_msg.attr['FLAGS']
server_msg_uids << uid
unless uids_to_be_deleted.include?(uid)
uids_to_be_fetched << uid
else
if flags.member?(:Seen) && cached_unread_uids.include?(uid)
uids_read << uid
elsif !flags.member?(:Seen) && cached_read_uids.include?(uid)
uids_unread << uid
end
end
uids_to_be_deleted.delete(uid)
} unless server_messages.nil?
ImapMessage.delete_all(["username = ? and folder_name = ? and uid in ( ? )", @username, @name, uids_to_be_deleted]) unless uids_to_be_deleted.empty?
ImapMessage.update_all('unread = 0', ["username = ? and folder_name = ? and uid in ( ? )", @username, @name, uids_read]) unless uids_read.empty?
ImapMessage.update_all('unread = 1', ["username = ? and folder_name = ? and uid in ( ? )", @username, @name, uids_unread]) unless uids_unread.empty?
# fetch and store not cached messages
unless uids_to_be_fetched.empty?
imapres = @mailbox.imap.uid_fetch(uids_to_be_fetched, @@fetch_attr)
imapres.each { |cache|
envelope = cache.attr['ENVELOPE'];
message = ImapMessage.create( :folder_name => @name,
:username => @username,
:msg_id => envelope.message_id,
:uid => cache.attr['UID'],
:from_addr => envelope.from,
:to_addr => envelope.to,
:subject => envelope.subject,
:content_type => cache.attr['BODYSTRUCTURE'].multipart? ? 'multipart' : 'text',
:date => envelope.date,
:unread => !(cache.attr['FLAGS'].member? :Seen),
:size => cache.attr['RFC822.SIZE'])
}
end
@mcached = true
logger.debug("Synchonization done for folder #{@name} in #{Time.now - startSync} ms.")
end
def messages(offset = 0, limit = 10, sort = 'date desc')
# Synchronize first retrieval time
synchronize_cache unless @mcached
if limit == -1
@messages = ImapMessage.find(:all, :conditions => ["username = ? and folder_name = ?", @username, @name], :order => sort)
else
@messages = ImapMessage.find(:all, :conditions => ["username = ? and folder_name = ?", @username, @name], :order => sort, :limit => limit, :offset => offset )
end
end
def messages_search(query = ["ALL"], sort = 'date desc')
activate
uids = @mailbox.imap.uid_search(query)
if uids.size > 1
ImapMessage.find(:all, :conditions => ["username = ? and folder_name = ? and uid in ( ? )", @username, @name, uids], :order => sort )
elsif uids.size == 1
ImapMessage.find(:all, :conditions => ["username = ? and folder_name = ? and uid = ? ", @username, @name, uids.first], :order => sort )
else
return Array.new
end
end
def message(uid)
activate
message = ImapMessage.find(:first, :conditions => ["username = ? and folder_name = ? and uid = ?", @username, @name, uid])
message.set_folder(self)
message
end
def unseen
activate
load_total_unseen if !@cached
@unseen_messages
end
def total
activate
load_total_unseen if !@cached
@total_messages
end
def load_total_unseen
stat = @mailbox.imap.status(@name, ["MESSAGES", "UNSEEN"])
@total_messages, @unseen_messages = stat["MESSAGES"], stat['UNSEEN']
@cached = true
end
def update_status
@status ||= @mailbox.imap.status(@name, ["MESSAGES"])
end
def subscribe
@mailbox.imap.subscribe(@name)
end
end

View file

@ -0,0 +1,167 @@
require 'cdfutils'
module Mail2Screen
def mail2html(mail, msg_id)
footer = ""
parsed_body = create_body(mail, msg_id, footer)
ret = "<table class='messageheader' border='0' cellpadding='0' cellspacing='0' >\n"
ret << "<tbody>\n"
ret << " <tr><td class='label' nowrap='nowrap'>#{_('From')}:</td><td>#{address(mail.from_addrs, @msg_id)}</td></tr>\n"
ret << " <tr><td class='label' nowrap='nowrap'>#{_('To')}:</td><td>#{address(mail.to_addrs, @msg_id)}</td></tr>\n"
if @mail.cc_addrs
ret << " <tr><td class='label' nowrap='nowrap'>#{_('CC')}:</td><td>#{address(mail.cc_addrs, @msg_id)}</td></tr>\n"
end
if @mail.bcc_addrs
ret << " <tr><td class='label' nowrap='nowrap'>#{_('BCC')}:</td><td>#{address(mail.bcc_addrs, @msg_id)}</td></tr>\n"
end
ret << " <tr><td class='label' nowrap='nowrap'>#{_('Subject')}:</td><td>#{h(mime_encoded?(mail.subject) ? mime_decode(mail.subject) : mail.subject)}</dd>\n"
ret << " <tr><td class='label' nowrap='nowrap'>#{_('Date')}:</td><td>#{h message_date(mail.date)}</td></tr>\n"
if footer != ''
ret << " <tr><td class='label' nowrap='nowrap'>#{image_tag('attachment.png')}</td><td>#{footer}</td></tr>\n"
end
ret << " </tbody>\n"
ret << "</table>\n"
ret << "<div class='msgpart'>\n"
ret << parsed_body
ret << "</div>\n"
end
def create_body(mail, msg_id, footer)
charset = (mail.charset.nil? ? 'iso-8859-1' : mail.charset)
if mail.multipart?
ret = ""
if mail.content_type == 'multipart/alternative'
# take only HTML part
mail.parts.each { |part|
if part.content_type == "text/html" or part.multipart?
ret << create_body(part, msg_id, footer)
end
}
return ret
else
mail.parts.each { |part|
if part.multipart?
ret << create_body(part, msg_id, footer)
else
footer << ", " if footer != ''
footer << add_attachment(part.header['content-type'], msg_id)
if part.content_type == "text/plain" or part.content_type.nil?
charset = (part.charset.nil? ? charset : mail.charset)
ret << add_text(part, part.transfer_encoding, charset)
elsif part.content_type == "text/html"
charset = (part.charset.nil? ? charset : mail.charset)
ret << add_html(part, part.transfer_encoding, charset)
elsif part.content_type.include?("image/")
ctype = part.header['content-type']
ret << add_image(ctype, msg_id)
elsif part.content_type.include?("message/rfc822")
ret << "<br/>#{_('Follows attached message')}:<hr/>" << mail2html(TMail::Mail.parse(part.body), msg_id)
end
end
}
return ret
end
else
ret = ""
if mail.content_type == "text/plain" or mail.content_type.nil?
ret << add_text(mail, mail.transfer_encoding, charset)
elsif mail.content_type == "text/html"
ret << add_html(mail, mail.transfer_encoding, charset)
end
return ret
end
end
def add_text(part, encoding, charset)
CGI.escapeHTML(decode_part_text("#{part}", encoding, charset)).gsub(/\r\n/,"<br/>").gsub(/\r/, "<br/>").gsub(/\n/,"<br/>")
end
def add_html(part, encoding, charset)
strip_html(decode_part_text("#{part}", encoding, charset))
end
def decode_part_text(part_str, encoding, charset)
# Parse mail
header, text = "", ""
# Get header and body
#Content-type: text/plain; charset="ISO-8859-1"
#Content-transfer-encoding: quoted-printable
isBody = false
part_str.each_line { |line|
if isBody
text << line
else
if line.strip == ""
isBody = true
else
header << line
end
end
}
# Manage encoding
if not(encoding.nil?) and encoding.downcase == "quoted-printable"
ret = from_qp(text)
elsif not(encoding.nil?) and encoding.downcase == "base64"
ret = "#{text.unpack("m")}"
else
ret = text
end
# manage charset
if ret.nil? or charset.nil? or charset.downcase == "utf-8"
return ret
else
begin
return Iconv.conv("UTF-8",charset.downcase, ret)
rescue Exception => ex
RAILS_DEFAULT_LOGGER.debug("Exception occured #{ex}\n#{ex.backtrace.join('\n')}")
return ret
end
end
end
def add_attachment(content_type, msg_id)
filename = (content_type.nil? or content_type['name'].nil? ? "" : content_type['name'])
if filename == ""
""
else
"<span class='attachment'>&nbsp;<a href='/webmail/webmail/download?msg_id=#{msg_id}&ctype=" << CGI.escape(filename) << "'>#{filename}</a></span>"
end
end
def add_image(content_type, msg_id)
filename = (content_type.nil? or content_type['name'].nil? ? "" : content_type['name'])
"<hr/><span class='attachment'><br/><img src='/webmail/webmail/download?msg_id=#{msg_id}&ctype=" << CGI.escape(filename) << "' alt='#{filename}'/></span>"
end
def friendly_address(addr)
addr.kind_of?(Net::IMAP::Address) ? ((addr.name.nil? or addr.name.strip == "") ? "#{addr.mailbox}@#{addr.host}" : "#{(mime_encoded?(addr.name.strip) ? mime_decode(addr.name.to_s): addr.name.to_s)}<#{addr.mailbox}@#{addr.host}>") : ((addr.name.nil? or addr.name.strip == "") ? "#{addr.spec}" : "#{(mime_encoded?(addr.name.strip) ? mime_decode(addr.name.to_s): addr.name.to_s)}<#{addr.spec}>")
end
def friendly_address_or_name(addr)
addr.kind_of?(Net::IMAP::Address) ? ((addr.name.nil? or addr.name.to_s == "") ? "#{addr.mailbox}@#{addr.host}" : (mime_encoded?(addr.name.to_s) ? mime_decode(addr.name.to_s): addr.name.to_s)) : ((addr.name.nil? or addr.name.to_s == "") ? "#{addr.spec}" : (mime_encoded?(addr.name.to_s) ? mime_decode(addr.name.to_s): addr.name.to_s))
end
def add_to_contact(addr, msg_id)
"&nbsp;<a href='/contacts/contact/add_from_mail?cstr=#{CGI.escape(friendly_address(addr))}&retmsg=#{msg_id}'>Add to contacts</a>"
end
def short_address(addresses)
ret = ""
addresses.each { |addr| #split(/,\s*/)
ret << "," unless ret == ""
ret << CGI.escapeHTML(friendly_address_or_name(addr))
} unless addresses.nil?
ret
end
def address(addresses, msg_id)
ret = ""
addresses.each { |addr| #split(/,\s*/)
ret << "," unless ret == ""
ret << CGI.escapeHTML(friendly_address_or_name(addr)) << add_to_contact(addr, msg_id)
} unless addresses.nil?
return ret
end
end

View file

@ -0,0 +1,9 @@
require_association 'customer'
class MailPref < ActiveRecord::Base
belongs_to :customer
def MailPref.find_by_customer(customer_id)
find_first(["customer_id = #{customer_id}"])
end
end

View file

@ -0,0 +1,75 @@
require 'mail2screen'
class MailTransform
include Mail2Screen
def get_body(tmail, type)
@mail = tmail
footer = ""
msg_id = ""
ret = mail2html(tmail, msg_id)
ret = ret.gsub(/<br\/>/,"\n").gsub(/&nbsp;/, " ").gsub(/&lt;/, "<").gsub(/&gt;/, ">").gsub(/&amp;/, "&").gsub(/<hr\/>/, "\n").gsub(/\n/, "\n> ") if type == 'text/plain'
ret = ret.gsub(/\r\n/,"<br/>").gsub(/\r/, "<br/>").gsub(/\n/,"<br/>").gsub(/<br\/>/, "<br/>&gt;&nbsp;") unless type == 'text/plain'
return ret
end
def mail2html(mail, msg_id)
footer = ""
parsed_body = create_body(mail, msg_id, footer)
ret = "-----Original Message-----\n#{_('From')}:#{address(mail.from_addrs, @msg_id)}\n"
ret << "#{_('To')}:#{address(mail.to_addrs, @msg_id)}\n"
if @mail.cc_addrs
ret << " #{_('CC')}:#{address(mail.cc_addrs, @msg_id)}\n"
end
if @mail.bcc_addrs
ret << "#{_('BCC')}:#{address(mail.bcc_addrs, @msg_id)}\n"
end
ret << "#{_('Subject')}:#{mime_encoded?(mail.subject) ? mime_decode(mail.subject) : mail.subject}\n"
ret << "#{_('Date')}:#{message_date(mail.date)}\n"
ret << "\n"
ret << "\n"
ret << parsed_body
ret << "\n"
end
def message_date(datestr)
t = Time.now
begin
if datestr.kind_of?(String)
d = (Time.rfc2822(datestr) rescue Time.parse(value)).localtime
else
d = datestr
end
if d.day == t.day and d.month == t.month and d.year == t.year
d.strftime("%H:%M")
else
d.strftime("%Y-%m-%d")
end
rescue
begin
d = imap2time(datestr)
if d.day == t.day and d.month == t.month and d.year == t.year
d.strftime("%H:%M")
else
d.strftime("%Y-%m-%d")
end
rescue
datestr
end
end
end
# Overwrite some staff
def add_to_contact(addr, msg_id)
""
end
def add_attachment(content_type, msg_id)
""
end
def add_image(content_type, msg_id)
""
end
end

View file

@ -0,0 +1,51 @@
module MaildropSerializator
def serialize_to_file
mail_drop_filter = File.new(self.mail_filter_path, "w")
for filter in filters
mail_drop_filter << "# filter '#{filter.name}'\n"
mail_drop_filter << "if (#{filter_expressions(filter)})\n"
mail_drop_filter << "{\n"
mail_drop_filter << " exception {\n"
mail_drop_filter << " to #{dest_folder(filter)}\n"
mail_drop_filter << " }\n"
mail_drop_filter << "}\n"
end
mail_drop_filter.close()
end
private
def dest_folder(filter)
'$DEFAULT/'<<filter.destination_folder.sub(Regexp.new("(#{CDF::CONFIG[:mail_inbox]})(.*)"), '\2')<<"/"
end
def escape_expr_value(text)
text.gsub(".", "\\.").gsub("*", "\\*").gsub("[", "\\[").gsub("]", "\\]").gsub("(", "\\(").gsub(")", "\\)").
gsub("?", "\\?")
end
def filter_expressions(filter)
fe = ""
for exp in filter.expressions
post_flag = "h"
fe << " && " unless fe == ""
if exp.field_name == "^Body"
fe << "/"
post_flag = "b"
else
fe << "/#{exp.field_name}:"
end
if exp.operator == 'contains'
fe << ".*(#{escape_expr_value(exp.expr_value)})/"
else
# starts with
fe << "[ ]*(#{escape_expr_value(exp.expr_value)}).*/"
end
if exp.case_sensitive == 1
fe << "D" << post_flag
else
fe << post_flag
end
end
fe
end
end

View file

@ -0,0 +1,4 @@
# Webmail mapping
map.connect 'webmail', :controller => 'webmail/webmail', :action => 'messages'

View file

@ -0,0 +1,3 @@
class VirtualEmail < ActiveRecord::Base
def self.table_name() "cdf.wm_virtual" end
end

View file

@ -0,0 +1,5 @@
<ul class="contacts">
<% for contact in @contacts do -%>
<li class="contact"><%=h contact.fname %> <%= h contact.lname %> <div class='email'><div class="notviscode">&amp;lt;</div><%= h contact.email %><div class="notviscode">&amp;gt;</div></div></li>
<% end -%>
</ul>

View file

@ -0,0 +1,18 @@
<% @expression = @expressions[expr_counter] %>
<tr>
<td>
<select name="expression[<%=expr_counter%>][field_name]">
<%= options_for_select(CDF::CONFIG[:mail_filters_fields], @expressions[expr_counter].field_name)%>
</select></td>
<td>
<select name="expression[<%=expr_counter%>][operator]">
<%= options_for_select(CDF::CONFIG[:mail_filters_expressions], @expressions[expr_counter].operator)%>
</select></td>
<td>
<%= text_field 'expression', 'expr_value', 'index'=>expr_counter %>
</td>
<td>
<%= hidden_field "expression", 'case_sensitive', 'index'=>expr_counter %>
<%= check_box "expression", 'case_sensitive', {'onclick'=>'toggleCheckbox(this)', "index"=>expr_counter } %>&nbsp; <%= _('case sensitive') %>
</td>
</tr>

View file

@ -0,0 +1,19 @@
<tr>
<td><%=h @user.filters[filter_counter].name%></th>
<td>
<% if filter_counter > 0 %>
<%=link_filter_up(@user.filters[filter_counter].id)%>
<% else %>
&nbsp;
<% end %>
</td>
<td>
<% if filter_counter < @user.filters.size - 1 %>
<%=link_filter_down(@user.filters[filter_counter].id)%>
<% else %>
&nbsp;
<% end %>
</td>
<td><%=link_filter_edit(@user.filters[filter_counter].id)%></td>
<td><%=link_filter_delete(@user.filters[filter_counter].id)%></td>
</tr>

View file

@ -0,0 +1,14 @@
<tr class="<%= message_row.unread ? 'notseen': 'seen' %>">
<td class="mark"><input type="checkbox" name="messages[<%= message_row.uid %>]"/></td>
<% if @folder_name == CDF::CONFIG[:mail_sent] %>
<td class="to"><%= short_address(message_row.to_addr) %></td>
<% else %>
<td class="from"><%= short_address(message_row.from_addr) %></td>
<% end %>
<td class="subject"><div class='cutField'>
<%= link_to(parse_subject(message_row.subject) << "&nbsp;" , :controller=>'webmail', :action=>'message', :msg_id=>message_row.uid)%>
</td>
<td class="date"><%= message_date(message_row.date) %></td>
<td class="size"><%= message_size(message_row.size) %></td>
<td class="attachind"><%= message_row.content_type == 'multipart' ? image_tag('attachment.png') : '&nbsp;' %></td>
</tr>

View file

@ -0,0 +1,13 @@
<a href='#' onclick='toggle_msg_search(true);'>
<%=_('Search')%><img id='img_msg_search' alt='open' src='/images/list_<%=@srch_img_src%>.gif'/>
</a>
<div id="msg_search" class='<%=@srch_class%>'>
<%=_('Search in message field')%>&nbsp;
<select name="search_field">
<%= options_for_select(CDF::CONFIG[:mail_search_fields], @search_field)%>
</select>&nbsp;
<label for="search_value"><%=_('for')%></label>&nbsp;
<input type="text" name="search_value" value="<%=@search_value%>" size='16' id='search_value'/>&nbsp;
<%= submit_tag(_('Search'), :name=>'op')%>&nbsp;
<%= submit_tag(_('Show all'), :name=>'op')%>
</div>

View file

@ -0,0 +1,67 @@
<h1><%=_('Mailbox')%></h1>
<form name="composeMail" id='composeMail' action="/webmail/webmail/compose" enctype="multipart/form-data" method="post">
<div id="header">
<ul id="primary">
<li><%=link_folders%></li>
<li><span><%= _('Compose') %></span>
<ul id="secondary">
<li><%=link_compose_new%></li>
<li><a href='#' onclick="getFormField('composeMail').submit();"><%=_('Send')%></a></li>
<li><a href="#" onclick="chooseContacts();"><%= _('Choose addresses from contacts') %></a></li>
</ul>
</li>
<li><%=link_mail_prefs%></li>
<li><%=link_mail_filters%></li>
<li><%=link_main%></li>
</ul>
</div>
<div id="tab_main">
<div id="tab_content">
<div id="msg-compose">
<input type="hidden" id="mail_toc" name="mail[toc]" value="<%=@mail.toc%>"/>
<input type="hidden" id="mail_op" name="op" value="<%=_('Send')%>"/>
<div id="msghdr">
<table>
<tr><td><label for=''><%=_('To')%></label></td><td><%= text_field_with_auto_complete(:mail, :to, {"size"=>65}, :skip_style => true, :tokens=> ",") %></td></tr>
<tr><td><label for=''><%=_('CC')%></label></td><td><%= text_field_with_auto_complete(:mail, :cc, {"size"=>65}, :skip_style => true, :tokens=> ",") %></td></tr>
<tr><td><label for=''><%=_('BCC')%></label></td><td><%= text_field_with_auto_complete(:mail, :bcc, {"size"=>65}, :skip_style => true, :tokens=> ",") %></td></tr>
<tr><td><label for=''><%=_('Subject')%></label></td><td><%= text_field('mail', 'subject', {"size"=>65}) %></td></tr>
</table>
<%= hidden_field('mail', 'from') %>
<%= hidden_field('mail', 'content_type') %>
</div>
<div class="msgpart ct-text-plain">
<%= text_area "mail", "body", "rows"=>20, "cols"=>75 %>
</div>
<div class="msgpart">
<% if @mail.attachments and @mail.attachments.size > 0%>
<table class="attachments">
<% i = 0
@mail.attachments.each { |att| %>
<tr><td><%=attachment(att, i)%></td></tr>
<% i = i + 1
}%>
</table>
<% end %>
<hr/>
<label for="attachment"><%=_('Attachment')%>:</label><%=%><input type="file" name="attachment"/>
<input type="button" name="mail_add_attachement" value="<%=_('Add')%>"
onclick="getFormField('mail_op').value='<%=_('Add')%>';getFormField('composeMail').submit();">
</div>
</div>
</div></div>
</form>
<% content_for("scripts") { %>
<%= @additional_scripts %>
<% if ['text/html', 'multipart'].include?(@mail.content_type) %>
<%= include_tinymce %>
<% end %>
<%= javascript_include_tag "controls" %>
<%= javascript_include_tag "dragdrop" %>
<%= javascript_include_tag "effects" %>
<% %>
<% } %>

View file

@ -0,0 +1,28 @@
<h1><%=_('Mailbox')%></h1>
<div id="header">
<ul id="primary">
<li><span><%= _('Folders') %></span>
<ul id="secondary">
<li><%=link_refresh%></li>
</ul>
</li>
<li><%=link_send_mail%></li>
<li><%=link_mail_prefs%></li>
<li><%=link_mail_filters%></li>
<li><%=link_main%></li>
</ul>
</div>
<div id="tab_main">
<div id="tab_content">
<% content_for('sidebar') { %>
<div id="folders">
<h4><%=_('Folders')%><br/><%=link_manage_folders%></h4>
<hr/>
<%=_('Error occured obtaining connection to mail server. Please excuse us!')%>
</div>
<% } %>
<div id="messages">
</div>
</div></div>

View file

@ -0,0 +1,41 @@
<h1><%=_('Mailbox')%></h1>
<div id="header">
<ul id="primary">
<li><%=link_folders%></li>
<li><%=link_send_mail%></li>
<li><%=link_mail_prefs%></li>
<li><span><%= _('Filters') %></span></li>
<li><%=link_main%></li>
</ul>
</div>
<div id="tab_main">
<div id="tab_content">
<div id="filters">
<form action="/webmail/webmail/filter" method="post">
<%=hidden_field 'filter', 'id' %>
<table class="list" width='100%'>
<tr>
<td><label for="filter[name]"><%=_('Filter name')%></label></td>
<td colspan='3'><%=text_field 'filter', 'name' %></td>
</tr>
<tr><td colspan="4"><%=_('Messages matching')%></td></tr>
<%= render_partial_collection "expr", @expressions %>
<tr><td colspan="4"><input type="submit" name="op" value="<%=_('Add')%>"/></td></tr>
<tr>
<td><label for="filter[destination_folder]"><%=_('Will be placed in')%></label></td>
<td colspan='3'>
<select name="filter[destination_folder]">
<%= options_from_collection_for_select(@destfolders, 'name', 'name', @filter.destination_folder) %>
</select>
</td>
</tr>
<tr><td colspan="4">
<input type="submit" name="op" value="<%=_('Save')%>"/>
<input type="button" name="op" value="<%=_('Cancel')%>" onclick="changeLoc('/webmail/webmail/filters');"/>
</td></tr>
</table>
</form>
</div>
</div></div>

View file

@ -0,0 +1,39 @@
<h1><%=_('Mailbox')%></h1>
<div id="header">
<ul id="primary">
<li><%=link_folders%></li>
<li><%=link_send_mail%></li>
<li><%=link_mail_prefs%></li>
<li><span><%= _('Filters') %></span>
<ul id="secondary">
<li><%=link_filter_add%></li>
</ul>
</li>
<li><%=link_main%></li>
</ul>
</div>
<div id="tab_main">
<div id="tab_content">
<div id="filters">
<form action="/webmail/webmail/filters" method="post">
<% if @user.filters and @user.filters.size > 0 %>
<table class="list">
<tr>
<th><%=_('Filter name')%></th>
<th>&nbsp;</th>
<th>&nbsp;</th>
<th>&nbsp;</th>
<th>&nbsp;</th>
</tr>
<%= render_partial_collection "filter", @user.filters %>
</table>
<% end %>
</form>
<% if @flash['error'] %>
<div id="SystemError"><%= @flash['error'] %></div>
<% elsif @flash['status'] %>
<div id="SystemStatus"><%= @flash['status'] %></div>
<% end %>
</div>
</div></div>

View file

@ -0,0 +1,35 @@
<h1><%=_('Mailbox')%></h1>
<div id="header">
<ul id="primary">
<li><span><%= _('Folders') %></span>
<ul id="secondary">
<li><%=link_refresh%></li>
</ul>
</li>
<li><%=link_send_mail%></li>
<li><%=link_mail_prefs%></li>
<li><%=link_mail_filters%></li>
<li><%=link_main%></li>
</ul>
</div>
<div id="tab_main">
<div id="tab_content">
<% content_for('sidebar') { %>
<div id="folders">
<h4><%=_('Folders')%><br/><%=link_manage_folders%></h4>
<hr/>
<ul> <% for folder in @folders %>
<li><%=folder_link(folder)%></li> <% end %>
</ul>
</div>
<% } %>
<div id="messages">
<% if not(@request['msg_id'] == '') %>
<%= render_component(:controller => "webmail/webmail", :action => "message", :params => { 'msg_id' => @request['msg_id']})%>
<% else %>
<%= render_component(:controller => "webmail/webmail", :action => "messages") %>
<% end %>
</div>
</div></div>

View file

@ -0,0 +1,31 @@
<h1><%=_('Mailbox')%></h1>
<div id="header">
<ul id="primary">
<li><%=link_folders%></li>
<li><span><%= _('Compose') %></span>
<ul id="secondary">
<li><%=link_compose_new%></li>
</ul>
</li>
<li><%=link_mail_prefs%></li>
<li><%=link_mail_filters%></li>
<li><%=link_main%></li>
</ul>
</div>
<div id="tab_main">
<div id="tab_content">
<div class="msghdr">
<dl>
<dt><%=_('To')%></dt><dd><%= CGI.escapeHTML(@mail.to) %></dd>
<% if @mail.cc %>
<dt><%=_('CC')%></dt><dd><%= CGI.escapeHTML(@mail.cc) %></dd>
<% end
if @mail.bcc %>
<dt><%=_('BCC')%></dt><dd><%= CGI.escapeHTML(@mail.bcc) %></dd>
<% end %>
<dt><%=_('Subject')%></dt><dd><%= CGI.escapeHTML(@mail.subject) %></dd>
</dl>
</div>
</div>
</div>

View file

@ -0,0 +1,48 @@
<h1><%=_('Mailbox')%></h1>
<div id="header">
<ul id="primary">
<li><span><%= _('Folders') %></span>
<ul id="secondary">
<li><%=link_back_to_messages%></li>
</ul>
</li>
<li><%=link_send_mail%></li>
<li><%=link_mail_prefs%></li>
<li><%=link_mail_filters%></li>
<li><%=link_main%></li>
</ul>
</div>
<div id="tab_main">
<div id="tab_content">
<% content_for('sidebar') { %>
<div id="folders">
<h4><%=_('Add folder')%></h4>
<hr/>
<form action="/webmail/webmail/manage_folders" method="post">
<ul>
<li><label for='folder_name'><%=_('Name')%>:</label></li>
<li><input type="text" name="folder_name" value="" size="18" id='folder_name'/></li>
<li><input type="submit" name="op" value="<%=_('Add folder')%>"/></li>
</ul>
</form>
</div>
<% } %>
<div id="messages">
<div id='msg-fl-list'>
<table>
<thead><tr><th><%=_('Folder')%></th><th><%= _('Total messages') %></th><th><%= _('Unseen') %></th></tr></thead>
<tbody>
<% for folder in @folders %>
<tr>
<td><%=folder_manage_link(folder)%></td>
<td><%= folder.total %></td>
<td><%= folder.unseen > 0 ? "<b>#{folder.unseen}</b>" : "#{folder.unseen}" %></td></tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,48 @@
<h1><%=_('Mailbox')%></h1>
<div id="header">
<ul id="primary">
<li><span><%= _('Folders') %></span>
<ul id="secondary">
<li><%=link_refresh%></li>
</ul>
</li>
<li><%=link_send_mail%></li>
<li><%=link_mail_prefs%></li>
<li><%=link_mail_filters%></li>
<li><%=link_main%></li>
</ul>
</div>
<div id="tab_main">
<div id="tab_content">
<% content_for('sidebar') { %>
<div id="folders">
<h4><%=_('Folders')%><br/><%=link_manage_folders%></h4>
<hr/>
<ul> <% for folder in @folders %>
<li><%=folder_link(folder)%></li> <% end %>
</ul>
</div>
<% } %>
<div id="messages">
<div id="msgshow">
<div id="topmenu">
<ul class="actionmenu">
<li>
<%= link_to(_("&#171; Back to list"), :controller=>"/webmail/webmail", :action=>"messages") %>
</li>
<li><%=link_reply_to_sender(@msg_id)%></li>
<li><%=link_forward_message(@msg_id)%></li>
<li><%=link_flag_for_deletion(@msg_id)%></li>
<li><%=link_view_source(@msg_id)%></li>
</ul>
</div>
<%= mail2html(@mail, @msg_id) %>
</div>
</div>
</div></div>

View file

@ -0,0 +1,89 @@
<h1><%=_('Mailbox')%></h1>
<div id="header">
<ul id="primary">
<li><span><%= _('Folders') %></span>
<ul id="secondary">
<li><%=link_refresh%></li>
</ul>
</li>
<li><%=link_send_mail%></li>
<li><%=link_mail_prefs%></li>
<li><%=link_mail_filters%></li>
<li><%=link_main%></li>
</ul>
</div>
<div id="tab_main">
<div id="tab_content">
<% content_for('sidebar') { %>
<div id="folders">
<h4><%=_('Folders')%><br/><%=link_manage_folders%></h4>
<hr/>
<ul> <% for folder in @folders %>
<li><%=folder_link(folder)%></li> <% end %>
</ul>
</div>
<div style="text-align: center; padding: 10px;">
<input type="button" value="<%= _('Logout')%>" onclick="window.location='/login/logout'">
</div>
<% } %>
<div id="messages">
<div id="msglist">
<h2><%= @folder_name %></h2>
<%= start_form_tag({:controller=>'/webmail/webmail', :action=>'messages'})%>
<div class='notviscode'><input type="submit" name="op" value="<%=_('Search')%>" /></div>
<input type="hidden" name="page" value="<%=@page%>"/>
<a href='#' onclick='toggle_msg_operations(true);'>
<%=_('Operations')%><img id='img_msgops' alt='open' src='/images/list_<%=@ops_img_src%>.gif'/>
</a>
<div id="msgops" class='<%=@ops_class%>'>
<h4><%=_('Operations on marked messages')%></h4>
<span id="opch">
<%= submit_tag(_('delete'), :name=>'op')%>
<%= submit_tag(_('copy'), :name=> 'op')%>
<%= submit_tag(_('move'), :name=>'op')%>
<%= submit_tag(_('mark read'), :name=>'op')%>
<%= submit_tag(_('mark unread'), :name=>'op')%>
</span><br/>
<span id="destp">
<%=_('Destination for move and copy operations')%>&nbsp;
<select name="cpdest" size="1">
<% for folder in @folders %>
<option value="<%=folder.name%>"><%=folder.name%></option>
<% end %>
</select>
</span>
</div>
<%= render_partial "search" %>
<%= page_navigation_webmail @pages if @pages.page_count > 1 %>
<table width='98%'>
<thead>
<tr>
<th width="1%"><input type="checkbox" name="allbox" onclick="checkAll(this.form)" style="margin: 0 0 0 4px" /></th>
<% if @folder_name == CDF::CONFIG[:mail_sent] %>
<th width="20%"><%= link_to(_('To'), :controller=>'/webmail/webmail', :action=>'messages', :op=>'SORT', :page=>@page, :scc=>'to_flat')%></th>
<% else %>
<th width="20%"><%= link_to(_('From'), :controller=>'/webmail/webmail', :action=>'messages', :op=>'SORT', :page=>@page, :scc=>'from_flat')%></th>
<% end%>
<th width='60%'><%= link_to(_('Subject'), :controller=>'/webmail/webmail', :action=>'messages', :op=>'SORT', :page=>@page, :scc=>'subject')%></th>
<th><%= link_to(_('Date'), :controller=>'/webmail/webmail', :action=>'messages', :op=>'SORT', :page=>@page, :scc=>'date')%></th>
<th><%= link_to(_('Size'), :controller=>'/webmail/webmail', :action=>'messages', :op=>'SORT', :page=>@page, :scc=>'size')%></th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
<% for message in @messages %>
<%= render_partial 'webmail/webmail/message_row', message %>
<% end %>
</tbody>
</table>
<%= page_navigation_webmail @pages if @pages.page_count > 1 %>
</form>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1 @@
No attachment found!

View file

@ -0,0 +1,52 @@
<h1><%=_('Mailbox')%></h1>
<div id="header">
<ul id="primary">
<li><%=link_folders%></li>
<li><%=link_send_mail%></li>
<li><span><%= _('Preferences') %></span></li>
<li><%=link_mail_filters%></li>
<li><%=link_main%></li>
</ul>
</div>
<div id="tab_main">
<div id="tab_content">
<div id="prefs">
<form action="/webmail/webmail/prefs" method="post">
<%= hidden_field "mailpref", "id" %>
<%= hidden_field "mailpref", "customer_id" %>
<table class="edit">
<%= form_input(:text_field, 'customer', 'fname', _('First name'), 'class'=>'two_columns') %>
<%= form_input(:text_field, 'customer', 'lname', _('Last name'), 'class'=>'two_columns') %>
<tr class="two_rows">
<td><label><%=_('Send type message')%></label></td>
<td><select name="mailpref[mail_type]">
<%= options_for_select(CDF::CONFIG[:mail_send_types], @mailpref.mail_type)%>
</select></td>
</tr>
<tr class="two_rows">
<td><label><%=_('Messages per page')%></label></td>
<td><select name="mailpref[wm_rows]">
<%= options_for_select(CDF::CONFIG[:mail_message_rows], @mailpref.wm_rows)%>
</select></td>
</tr>
<tr class="two_rows">
<td><label for="mailpref_check_external_mail"><%=_('Check external mail?')%></label></td>
<td>
<%=check_box('mailpref', 'check_external_mail')%>
<%=_('Note that by selecting this option webmail system will try to log you using your original email on a local server.')%>
</td>
</tr>
<tr class="two_rows">
<td colspan='2' class="buttonBar">
<input type="submit" name="op" value="<%=_('Save')%>"/>
<input type="button" value="<%=_('Cancel')%>" onclick="window.location='/webmail/webmail/folders'"/>
</td>
</tr>
</table>
</form>
</div>
</div>
</div>

View file

@ -0,0 +1,13 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title><%=_('Mailr')%></title>
<link rel="stylesheet" href="/stylesheets/webmail/webmail.css" type="text/css" media="screen" />
</head>
<body>
<div id="msg_source">
<%=@msg_source%>
</div>
</body>
</html>

View file

@ -0,0 +1,494 @@
require 'cdfmail'
require 'net/smtp'
require 'net/imap'
require 'mail2screen'
require 'ezcrypto'
class Webmail::WebmailController < ApplicationController
uses_component_template_root
# Administrative functions
before_filter :login_required
before_filter :obtain_cookies_for_search_and_nav, :only=>[:messages]
layout "public", :except => [:view_source, :download]
before_filter :load_imap_session
after_filter :close_imap_session
model :filter, :expression, :mail_pref, :customer
BOOL_ON = "on"
def index
redirect_to(:action=>"messages")
end
def error_connection
end
def refresh
@mailbox.reload
@folders = @mailbox.folders
redirect_to(:action=>'messages')
end
def manage_folders
if operation_param == _('Add folder')
@mailbox.create_folder(CDF::CONFIG[:mail_inbox]+"."+@params["folder_name"])
elsif operation_param == _('(Delete)')
@mailbox.delete_folder(@params["folder_name"])
elsif operation_param == _('(Subscribe)')
elsif operation_param == _('(Select)')
end
@folders = @mailbox.folders
end
def messages
@session["return_to"] = nil
@search_field = @params['search_field']
@search_value = @params['search_value']
# handle sorting - tsort session field contains last reverse or no for field
# and lsort - last sort field
if @session['tsort'].nil? or @session['lsort'].nil?
@session['lsort'] = "DATE"
@session['tsort'] = {"DATE" => true, "FROM" => true, "SUBJECT" => true, "TO" => false}
end
case operation_param
when _('copy') # copy
msg_ids = []
messages_param.each { |msg_id, bool|
msg_ids << msg_id.to_i if bool == BOOL_ON and dst_folder != @folder_name } if messages_param
folder.copy_multiple(msg_ids, dst_folder) if msg_ids.size > 0
when _('move') # move
msg_ids = []
messages_param.each { |msg_id, bool|
msg_ids << msg_id.to_i if bool == BOOL_ON and dst_folder != @folder_name } if messages_param
folder.move_multiple(msg_ids, dst_folder) if msg_ids.size > 0
when _('delete') # delete
msg_ids = []
messages_param.each { |msg_id, bool| msg_ids << msg_id.to_i if bool == BOOL_ON } if messages_param
folder.delete_multiple(msg_ids) if msg_ids.size > 0
when _('mark read') # mark as read
messages_param.each { |msg_id, bool| msg = folder.mark_read(msg_id.to_i) if bool == BOOL_ON } if messages_param
when _('mark unread') # mark as unread
messages_param.each { |msg_id, bool| msg = folder.mark_unread(msg_id.to_i) if bool == BOOL_ON } if messages_param
when "SORT"
@session['lsort'] = sort_query = @params["scc"]
@session['tsort'][sort_query] = (@session['tsort'][sort_query]? false : true)
@search_field, @search_value = @session['search_field'], @session['search_value']
when _('Search') # search
@session['search_field'] = @search_field
@session['search_value'] = @search_value
when _('Show all') # search
@session['search_field'] = @search_field = nil
@session['search_value'] = @search_value = nil
else
# get search criteria from session
@search_field = @session['search_field']
@search_value = @session['search_value']
end
sort_query = @session['lsort']
reverse_sort = @session['tsort'][sort_query]
query = ["ALL"]
@page = @params["page"]
@page ||= @session['page']
@session['page'] = @page
if @search_field and @search_value and not(@search_field.strip() == "") and not(@search_value.strip() == "")
@pages = Paginator.new self, 0, get_mail_prefs.wm_rows, @page
@messages = folder.messages_search([@search_field, @search_value], sort_query + (reverse_sort ? ' desc' : ' asc'))
else
@pages = Paginator.new self, folder.total, get_mail_prefs.wm_rows, @page
@messages = folder.messages(@pages.current.first_item - 1, get_mail_prefs.wm_rows, sort_query + (reverse_sort ? ' desc' : ' asc'))
end
end
def delete
@msg_id = msg_id_param.to_i
folder.messages().delete(@msg_id)
redirect_to(:action=>"messages")
end
def reply # not ready at all
@msg_id = msg_id_param.to_i
@imapmail = folder.message(@msg_id)
fb = @imapmail.full_body
@tmail = TMail::Mail.parse(fb)
@mail = prepare_mail
@mail.reply(@tmail, fb, get_mail_prefs.mail_type)
render_action("compose")
end
def forward
@msg_id = msg_id_param.to_i
@imapmail = folder.message(@msg_id)
fb = @imapmail.full_body
@tmail = TMail::Mail.parse(fb)
@mail = prepare_mail
@mail.forward(@tmail, fb)
render_action("compose")
end
def compose
if @mail.nil?
operation = operation_param
if operation == _('Send')
@mail = create_mail
encmail = @mail.send_mail
get_imap_session
@mailbox.message_sent(encmail)
# delete temporary files (attachments)
@mail.delete_attachments()
return render("webmail/webmail/mailsent")
elsif operation == _('Add')
@mail = create_mail
attachment = CDF::Attachment.new(@mail)
attachment.file = @params['attachment']
else
# default - new email create
@mail = create_mail
end
end
end
def empty # empty trash folder (works for any one else :-))
folder.messages(0, -1).each{ |message|
folder.delete(message)
}
folder.expunge
redirect_to(:action=>"messages")
end
def message
@msg_id = msg_id_param
@imapmail = folder.message(@msg_id)
folder.mark_read(@imapmail.uid) if @imapmail.unread
@mail = TMail::Mail.parse(@imapmail.full_body)
end
def download
msg_id = msg_id_param
imapmail = folder.message(msg_id)
mail = TMail::Mail.parse(imapmail.full_body)
if mail.multipart?
get_parts(mail).each { |part|
return send_part(part) if part.header and part.header['content-type']['name'] == @params['ctype']
}
render("webmail/webmail/noattachment")
else
render("webmail/webmail/noattachment")
end
end
def prefs
@customer = Customer.find(logged_customer)
if not(@mailpref = MailPref.find_by_customer(logged_customer))
@mailpref = MailPref.create("customer_id"=>logged_customer)
end
if @params['op'] == _('Save')
if @params['customer']
@customer.fname = @params['customer']['fname']
@customer.lname = @params['customer']['lname']
@customer.save
end
@mailpref.attributes = @params["mailpref"]
@mailpref.save
@session["wmimapseskey"] = nil
redirect_to(:action=>"messages")
end
end
# Message filters management
def filters
end
def filter
if @params['op']
@filter = Filter.new(@params['filter'])
@filter.customer_id = logged_customer
@params['expression'].each { |index, expr| @filter.expressions << Expression.new(expr) unless expr["expr_value"].nil? or expr["expr_value"].strip == "" }
case @params['op']
when _('Add')
@filter.expressions << Expression.new
when _('Save')
if @params['filter']['id'] and @params['filter']['id'] != ""
@sf = Filter.find(@params['filter']['id'])
@sf.name, @sf.destination_folder = @filter.name, @filter.destination_folder
@sf.expressions.each{|expr| Expression.delete(expr.id) }
@filter.expressions.each {|expr| @sf.expressions << Expression.create(expr.attributes) }
else
@sf = Filter.create(@filter.attributes)
@sf.order_num = @user.filters.size
@filter.expressions.each {|expr| @sf.expressions << Expression.create(expr.attributes) }
end
# may be some validation will be needed
@sf.save
@user.serialize_to_file
return redirect_to(:action=>"filters")
end
@expressions = @filter.expressions
else
@filter = Filter.find(@params["id"]) if @params["id"]
@expressions = @filter.expressions
end
@destfolders = get_to_folders
end
def filter_delete
Filter.delete(@params["id"])
# reindex other filters
@user = Customer.find(logged_customer)
findex = 0
@user.filters.each { |filter|
findex = findex + 1
filter.order_num = findex
filter.save
}
@user.serialize_to_file
redirect_to :action=>"filters"
end
def filter_up
filt = @user.filters.find(@params['id'])
ufilt = @user.filters.find_all("order_num = #{filt.order_num - 1}").first
ufilt.order_num = ufilt.order_num + 1
filt.order_num = filt.order_num - 1
ufilt.save
filt.save
@user.serialize_to_file
redirect_to :action=>"filters"
end
def filter_down
filt = Filter.find(@params["id"])
dfilt = @user.filters[filt.order_num]
dfilt.order_num = dfilt.order_num - 1
filt.order_num = filt.order_num + 1
dfilt.save
filt.save
@user.serialize_to_file
redirect_to :action=>"filters"
end
def filter_add
@filter = Filter.new
@filter.expressions << Expression.new
@expressions = @filter.expressions
@destfolders = get_to_folders
render_action("filter")
end
# end of filters
def view_source
@msg_id = msg_id_param.to_i
@imapmail = folder.message(@msg_id)
@msg_source = CGI.escapeHTML(@imapmail.full_body).gsub("\n", "<br/>")
end
def auto_complete_for_mail_to
auto_complete_responder_for_contacts params[:mail][:to]
end
def auto_complete_for_mail_cc
auto_complete_responder_for_contacts params[:mail][:cc]
end
def auto_complete_for_mail_bcc
auto_complete_responder_for_contacts params[:mail][:bcc]
end
private
def auto_complete_responder_for_contacts(value)
# first split by "," and take last name
searchName = value.split(',').last.strip
# if there are 2 names search by them
if searchName.split.size > 1
fname, lname = searchName.split.first, searchName.split.last
conditions = ['customer_id = ? and LOWER(fname) LIKE ? and LOWER(lname) like ?', logged_customer, fname.downcase + '%', lname.downcase + '%']
else
conditions = ['customer_id = ? and LOWER(fname) LIKE ?', logged_customer, searchName.downcase + '%']
end
@contacts = Contact.find(:all, :conditions => conditions, :order => 'fname ASC',:limit => 8)
render :partial => 'contacts'
end
protected
def additional_scripts()
'<link rel="stylesheet" href="/stylesheets/webmail/webmail.css" type="text/css" media="screen" />'<<
'<script type="text/javascript" src="/javascripts/webmail.js"></script>'
end
private
def get_upass
if CDF::CONFIG[:crypt_session_pass]
EzCrypto::Key.decrypt_with_password(CDF::CONFIG[:encryption_password], CDF::CONFIG[:encryption_salt], @session["wmp"])
else
# retrun it plain
@session["wmp"]
end
end
def get_to_folders
res = Array.new
@folders.each{|f| res << f unless f.name == CDF::CONFIG[:mail_sent] or f.name == CDF::CONFIG[:mail_inbox] }
res
end
def load_imap_session
return if ['compose', 'prefs', 'error_connection'].include?(action_name)
get_imap_session
end
def get_imap_session
begin
@mailbox = IMAPMailbox.new
uname = (get_mail_prefs.check_external_mail == 1 ? user.email : user.local_email)
upass = get_upass
@mailbox.connect(uname, upass)
load_folders
rescue Exception => ex
logger.error("Exception on loggin webmail session - #{ex} - #{ex.backtrace.join("\t\n")}")
render_action "error_connection"
end
end
def close_imap_session
return if @mailbox.nil? or not(@mailbox.connected)
@mailbox.disconnect
@mailbox = nil
end
def have_to_load_folders?
return true if ['messages', 'delete', 'reply', 'forward', 'empty', 'message', 'download',
'filter', 'filter_add', 'view_source'].include?(action_name)
return false
end
def load_folders
if have_to_load_folders?()
if @params["folder_name"]
@folder_name = @params["folder_name"]
else
@folder_name = @session["folder_name"] ? @session["folder_name"] : CDF::CONFIG[:mail_inbox]
end
@session["folder_name"] = @folder_name
@folders = @mailbox.folders if @folders.nil?
end
end
def create_mail
m = CDF::Mail.new(user.mail_temporary_path)
if @params["mail"]
ma = @params["mail"]
m.body, m.content_type, m.from, m.to, m.cc, m.bcc, m.subject = ma["body"], ma["content_type"], ma["from"], ma["to"], ma["cc"], ma["bcc"], ma["subject"]
if @params["att_files"]
att_files, att_tfiles, att_ctypes = @params["att_files"], @params["att_tfiles"], @params["att_ctypes"]
att_files.each {|i, value|
att = CDF::Attachment.new(m)
att.filename, att.temp_filename, att.content_type = value, att_tfiles[i], att_ctypes[i]
}
end
else
m.from, m.content_type = user.friendlly_local_email, get_mail_prefs.mail_type
end
m.customer_id = logged_customer
m
end
def prepare_mail
m = CDF::Mail.new(user.mail_temporary_path)
m.from, m.content_type = user.friendlly_local_email, get_mail_prefs.mail_type
m
end
def user
@user = Customer.find(logged_customer) if @user.nil?
@user
end
def get_mail_prefs
if not(@mailprefs)
if not(@mailprefs = MailPref.find_by_customer(logged_customer))
@mailprefs = MailPref.create("customer_id"=>logged_customer)
end
end
@mailprefs
end
def send_part(part)
if part.content_type == "text/html"
disposition = "inline"
elsif part.content_type.include?("image/")
disposition = "inline"
else
disposition = "attachment"
end
@headers['Content-Length'] = part.body.size
@response.headers['Accept-Ranges'] = 'bytes'
@headers['Content-type'] = part.content_type.strip
@headers['Content-Disposition'] = disposition << %(; filename="#{part.header['content-type']['name']}")
render_text part.body
end
def get_parts(mail)
parts = Array.new
parts << mail
mail.parts.each { |part|
if part.multipart?
parts = parts.concat(get_parts(part))
elsif part.content_type and part.content_type.include?("rfc822")
parts = parts.concat(get_parts(TMail::Mail.parse(part.body))) << part
else
parts << part
end
}
parts
end
def obtain_cookies_for_search_and_nav
@srch_class = ((cookies['_wmlms'] and cookies['_wmlms'] == 'closed') ? 'closed' : 'open')
@srch_img_src = ((cookies['_wmlms'] and cookies['_wmlms'] == 'closed') ? 'closed' : 'opened')
@ops_class = ((cookies['_wmlmo'] and cookies['_wmlmo'] == 'closed') ? 'closed' : 'open')
@ops_img_src = ((cookies['_wmlmo'] and cookies['_wmlmo'] == 'closed') ? 'closed' : 'opened')
end
###################################################################
### Some fixed parameters and session variables
###################################################################
def folder
@folders[@folder_name]
end
def msg_id_param
@params["msg_id"]
end
def messages_param
@params["messages"]
end
def dst_folder
@params["cpdest"]
end
def operation_param
@params["op"]
end
end

View file

@ -0,0 +1,211 @@
require 'cdfutils'
require 'mail2screen'
module Webmail::WebmailHelper
include Mail2Screen
def link_folders
link_to(_('Folders'), :controller=>"/webmail/webmail", :action=>"messages")
end
def link_send_mail
link_to(_('Compose'), :controller=>"/webmail/webmail", :action=>"compose")
end
def link_compose_new
link_to(_('Compose new mail'), :controller=>"/webmail/webmail", :action=>"compose")
end
def link_refresh
link_to(_('Refresh'), :controller=>"/webmail/webmail", :action=>"refresh")
end
def link_message_list
link_to(_('Message list'), :controller=>"/webmail/webmail", :action=>"messages")
end
def link_reply_to_sender(msg_id)
link_to(_('Reply'), :controller=>"/webmail/webmail", :action=>"reply", :params=>{"msg_id"=>msg_id})
end
def link_forward_message(msg_id)
link_to(_('Forward'), :controller=>"/webmail/webmail", :action=>"forward", :params=>{"msg_id"=>msg_id})
end
def link_flag_for_deletion(msg_id)
link_to(_('Delete'), :controller=>"/webmail/webmail", :action=>"delete", :params=>{"msg_id"=>msg_id})
end
def link_view_source(msg_id)
link_to(_('View source'), {:controller=>"/webmail/webmail", :action=>"view_source", :params=>{"msg_id"=>msg_id}}, {'target'=>"_blank"})
end
def link_manage_folders
link_to(_('add/edit'), :controller=>"/webmail/webmail", :action=>"manage_folders")
end
def link_back_to_messages
link_to("&#171;" << _('Back to messages'), :controller=>"/webmail/webmail", :action=>"messages")
end
def link_mail_prefs
link_to(_('Preferences'), :controller=>"/webmail/webmail", :action=>"prefs")
end
def link_mail_filters
link_to(_('Filters'), :controller=>"/webmail/webmail", :action=>"filters")
end
def link_filter_add
link_to(_('Add filter'), :controller=>'/webmail/webmail', :action=>'filter_add')
end
def folder_link(folder)
if folder.attribs.include? :Noselect
return folder.name
end
unseen_messages = folder.unseen
if unseen_messages > 0
fn = "#{short_fn(folder)} (#{folder.unseen})"
else
fn = "#{short_fn(folder)}"
end
if folder.name == CDF::CONFIG[:mail_trash]
(unseen_messages > 0 ? "<b>" : "" ) <<
link_to( fn, :controller=>"/webmail/webmail", :action=>"messages", :params=>{"folder_name"=>folder.name}) <<
"&nbsp;" << link_to(_('(Empty)'), {:controller=>"/webmail/webmail", :action=>"empty", :params=>{"folder_name"=>folder.name}}, :confirm => _('Do you really want to empty trash?')) <<
(unseen_messages > 0 ? "</b>" : "" )
else
(unseen_messages > 0 ? "<b>" : "" ) <<
link_to( fn, :controller=>"/webmail/webmail", :action=>"messages", :params=>{"folder_name"=>folder.name}) <<
(unseen_messages > 0 ? "</b>" : "" )
end
end
def short_fn(folder)
if folder.name.include? folder.delim
"&nbsp; &nbsp;" + folder.name.split(folder.delim).last
else
folder.name
end
end
def folder_manage_link(folder)
if folder.name == CDF::CONFIG[:mail_trash] or folder.name == CDF::CONFIG[:mail_inbox] or folder.name == CDF::CONFIG[:mail_sent]
short_fn(folder)
else
return short_fn(folder) +
("&nbsp;" + link_to(_('(Delete)'), :controller=>"/webmail/webmail", :action=>"manage_folders", :params=>{"op"=>_('(Delete)'), "folder_name"=>folder.name}))
end
end
def message_date(datestr)
t = Time.now
begin
if datestr.kind_of?(String)
d = (Time.rfc2822(datestr) rescue Time.parse(value)).localtime
else
d = datestr
end
if d.day == t.day and d.month == t.month and d.year == t.year
d.strftime("%H:%M")
else
d.strftime("%Y-%m-%d")
end
rescue
begin
d = imap2time(datestr)
if d.day == t.day and d.month == t.month and d.year == t.year
d.strftime("%H:%M")
else
d.strftime("%Y-%m-%d")
end
rescue
datestr
end
end
end
def attachment(att, index)
ret = "#{att.filename}"
# todo: add link to delete attachment
#ret <<
ret << "<input type='hidden' name='att_files[#{index}]' value='#{att.filename}'/>"
ret << "<input type='hidden' name='att_tfiles[#{index}]' value='#{att.temp_filename}'/>"
ret << "<input type='hidden' name='att_ctypes[#{index}]' value='#{att.content_type}'/>"
end
def link_filter_up(filter_id)
link_to(_('Up'), :controller=>"/webmail/webmail", :action=>"filter_up", :id=>filter_id)
end
def link_filter_down(filter_id)
link_to(_('Down'), :controller=>"/webmail/webmail", :action=>"filter_down", :id=>filter_id)
end
def link_filter_edit(filter_id)
link_to(_('Edit'), :controller=>"/webmail/webmail", :action=>"filter", :id=>filter_id)
end
def link_filter_delete(filter_id)
link_to(_('Delete'), :controller=>"/webmail/webmail", :action=>"filter_delete", :id=>filter_id)
end
def page_navigation_webmail(pages)
nav = "<p class='paginator'><small>"
nav << "(#{pages.length} #{_('Pages')}) &nbsp; "
window_pages = pages.current.window.pages
nav << "..." unless window_pages[0].first?
for page in window_pages
if pages.current == page
nav << page.number.to_s << " "
else
nav << link_to(page.number, :controller=>"/webmail/webmail", :action=>'messages', :page=>page.number) << " "
end
end
nav << "..." unless window_pages[-1].last?
nav << " &nbsp; "
nav << link_to(_('First'), :controller=>"/webmail/webmail", :action=>'messages', :page=>@pages.first.number) << " | " unless @pages.current.first?
nav << link_to(_('Prev'), :controller=>"/webmail/webmail", :action=>'messages', :page=>@pages.current.previous.number) << " | " if @pages.current.previous
nav << link_to(_('Next'), :controller=>"/webmail/webmail", :action=>'messages', :page=>@pages.current.next.number) << " | " if @pages.current.next
nav << link_to(_('Last'), :controller=>"/webmail/webmail", :action=>'messages', :page=>@pages.last.number) << " | " unless @pages.current.last?
nav << "</small></p>"
return nav
end
def parse_subject(subject)
begin
if mime_encoded?(subject)
if mime_decode(subject) == ''
_('(No subject)')
else
mime_decode(subject)
end
else
if from_qp(subject) == ''
_('(No subject)')
else
from_qp(subject)
end
end
rescue Exception => ex
RAILS_DEFAULT_LOGGER.debug('Exception occured - #{ex}')
return ""
end
end
def message_size(size)
if size / (1024*1024) > 0
return "#{(size / (1024*1024)).round}&nbsp;MB"
elsif size / 1024 > 0
return "#{(size / (1024)).round}&nbsp;KB"
else
return "#{size}&nbsp;B"
end
end
end

17
config/boot.rb Normal file
View file

@ -0,0 +1,17 @@
unless defined?(RAILS_ROOT)
root_path = File.join(File.dirname(__FILE__), '..')
unless RUBY_PLATFORM =~ /mswin32/
require 'pathname'
root_path = Pathname.new(root_path).cleanpath.to_s
end
RAILS_ROOT = root_path
end
if File.directory?("#{RAILS_ROOT}/vendor/rails")
require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
else
require 'rubygems'
require 'initializer'
end
Rails::Initializer.run(:set_load_path)

View file

@ -0,0 +1,20 @@
development:
adapter: mysql
database: mailr_dev
host: localhost
username: root
password:
test:
adapter: mysql
database: mailr_tests
host: localhost
username: root
password:
production:
adapter: mysql
database: mailr
host: localhost
username: root
password:

51
config/default_site.rb Normal file
View file

@ -0,0 +1,51 @@
# Site-specific parameters
# These are Mailr constants. If you want to overwrite
# some of them - create file config/site.rb
# containing new constants in LOCALCONFIG module variable - they
# will overwrite default values. Example site.rb:
#
# module CDF
# LOCALCONFIG = {
# :mysql_version => '4.1',
# :default_encoding => 'utf-8',
# :imap_server => 'your.imap.server',
# :imap_auth => 'LOGIN'
# }
# end
module CDF
CONFIG = {
:mysql_version => '4.0',
:default_language => 'en',
:default_encoding => 'ISO-8859-1',
:mail_charset => 'ISO-8859-1',
:mail_inbox => 'INBOX',
:mail_trash => 'INBOX.Trash',
:mail_sent => 'INBOX.Sent',
:mail_bulk_sent => "INBOX.SentBulk",
:mail_spam => "INBOX.Spam",
:mail_temp_path => 'mail_temp',
:mail_filters_path => '/home/vmail/mailfilters',
:mail_send_types => {"Plain text" => "text/plain", "HTML"=>"text/html", "HTML and PlainText" => "multipart"},
:mail_message_rows => [5, 10, 15, 20, 25, 30, 50],
:mail_filters_fields => {'From' => '^From', 'To' => '^To', 'CC' => '^CC', 'Subject' => '^Subject', 'Body' => '^Body'},
:mail_filters_expressions => ['contains', 'starts with'],
:mail_search_fields => ['FROM', 'TO', 'CC', 'SUBJECT', 'BODY'],
:temp_file_location => ".",
:contacts_per_page => 15,
:contact_letters => ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'],
:upload_file_temp_path => "/tmp",
:imap_server => 'localhost',
:imap_use_ssl => false,
:imap_port => 143,
:imap_auth => 'PLAIN', # 'LOGIN', 'NOAUTH'
:encryption_salt => 'EnCr1p10n$@lt',
:encryption_password => '$0MeEncr1pt10nP@a$sw0rd',
:debug_imap => false,
:crypt_session_pass => true, # Set it to false (in site.rb) if you get any error messages like
# "Unsupported cipher algorithm (aes-128-cbc)." - meaning that OpenSSL modules for crypt algo is not loaded. Setting it to false will store password in session in plain format!
:send_from_domain => nil, # Set this variable to your domain name in site.rb if you make login to imap only with username (without '@domain')
:imap_bye_timeout_retry_seconds => 2
}
end

74
config/environment.rb Normal file
View file

@ -0,0 +1,74 @@
# Be sure to restart your webserver when you modify this file.
# Uncomment below to force Rails into production mode
# (Use only when you can't set environment variables through your web/app server)
# ENV['RAILS_ENV'] = 'production'
# Bootstrap the Rails environment, frameworks, and default configuration
require File.join(File.dirname(__FILE__), 'boot')
Rails::Initializer.run do |config|
# Skip frameworks you're not going to use
# config.frameworks -= [ :action_web_service, :action_mailer ]
# Add additional load paths for your own custom dirs
config.load_paths += %W( #{RAILS_ROOT}/vendor/ezcrypto-0.1.1/lib )
# Force all environments to use the same logger level
# (by default production uses :info, the others :debug)
# config.log_level = :debug
# Use the database for sessions instead of the file system
# (create the session table with 'rake create_sessions_table')
config.action_controller.session_store = :active_record_store
# Enable page/fragment caching by setting a file-based store
# (remember to create the caching directory and make it readable to the application)
# config.action_controller.fragment_cache_store = :file_store, "#{RAILS_ROOT}/cache"
# Activate observers that should always be running
# config.active_record.observers = :cacher, :garbage_collector
# Make Active Record use UTC-base instead of local time
config.active_record.default_timezone = :utc
# Use Active Record's schema dumper instead of SQL when creating the test database
# (enables use of different database adapters for development and test environments)
# config.active_record.schema_format = :ruby
# See Rails::Configuration for more options
end
# Add new inflection rules using the following format
# (all these examples are active by default):
# Inflector.inflections do |inflect|
# inflect.plural /^(ox)$/i, '\1en'
# inflect.singular /^(ox)en/i, '\1'
# inflect.irregular 'person', 'people'
# inflect.uncountable %w( fish sheep )
# end
# Include your application configuration below
default_config_path = 'config/default_site'
default_config = File.join(RAILS_ROOT, default_config_path)
require default_config
begin
require File.join(RAILS_ROOT, 'config/site')
CDF::CONFIG.update(CDF::LOCALCONFIG) if CDF::LOCALCONFIG
rescue LoadError
STDERR.puts 'WARNING: config/site.rb not found, using default settings from ' + default_config_path
end
require 'tmail_patch'
require 'gettext_extension'
$KCODE = 'u'
require 'jcode'
# set UTF8 as a client interface to Database
RAILS_DEFAULT_LOGGER.debug("Using MySQL Version #{CDF::CONFIG[:mysql_version]}")
ActiveRecord::Base.connection.execute("SET NAMES utf8", 'Set Client encoding to UTF8') if CDF::CONFIG[:mysql_version] == '4.1'
ActiveRecord::Base.connection.execute("set character set utf8", 'Set Connection encoding to UTF8') if CDF::CONFIG[:mysql_version] == '4.1'
require 'imapmailbox'
[IMAPMailbox, IMAPFolderList, IMAPFolder].each { |mod| mod.logger ||= RAILS_DEFAULT_LOGGER }

View file

@ -0,0 +1,17 @@
# In the development environment your application's code is reloaded on
# every request. This slows down response time but is perfect for development
# since you don't have to restart the webserver when you make code changes.
config.cache_classes = false
# Log error messages when you accidentally call methods on nil.
config.whiny_nils = true
# Enable the breakpoint server that script/breakpointer connects to
config.breakpoint_server = true
# Show full error reports and disable caching
config.action_controller.consider_all_requests_local = true
config.action_controller.perform_caching = false
# Don't care if the mailer can't send
config.action_mailer.raise_delivery_errors = false

View file

@ -0,0 +1,17 @@
# The production environment is meant for finished, "live" apps.
# Code is not reloaded between requests
config.cache_classes = true
# Use a different logger for distributed setups
# config.logger = SyslogLogger.new
# Full error reports are disabled and caching is turned on
config.action_controller.consider_all_requests_local = false
config.action_controller.perform_caching = true
# Enable serving of images, stylesheets, and javascripts from an asset server
# config.action_controller.asset_host = "http://assets.example.com"
# Disable delivery errors if you bad email addresses should just be ignored
# config.action_mailer.raise_delivery_errors = false

View file

@ -0,0 +1,23 @@
# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!
config.cache_classes = true
# Log error messages when you accidentally call methods on nil.
config.whiny_nils = true
# Show full error reports and disable caching
config.action_controller.consider_all_requests_local = true
config.action_controller.perform_caching = false
# Tell ActionMailer not to deliver emails to the real world.
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
# Overwrite the default settings for fixtures in tests. See Fixtures
# for more details about these settings.
# config.transactional_fixtures = true
# config.instantiated_fixtures = false
# config.pre_loaded_fixtures = false

79
config/lighttpd.conf Normal file
View file

@ -0,0 +1,79 @@
server.port = 80
server.bind = "127.0.0.1"
#server.event-handler = "freebsd-kqueue"
server.modules = ( "mod_rewrite", "mod_fastcgi", )
server.indexfiles = ( "dispatch.fcgi" )
server.document-root = "public/"
server.error-handler-404 = "/dispatch.fcgi"
server.errorlog = "log/error.log"
url.rewrite = ( "^/$" => "/login", "^([^.]+)$" => "$1.html" )
#### fastcgi module
fastcgi.server = (
".fcgi" => (
"mailr" => (
"min-procs" => 2,
"max-procs" => 2,
"socket" => "/tmp/mailr1.socket",
"bin-path" => "public/dispatch.fcgi",
"idle-timeout" => 120,
"bin-environment" => ( "RAILS_ENV" => "production" )
)
)
)
mimetype.assign = (
".rpm" => "application/x-rpm",
".pdf" => "application/pdf",
".sig" => "application/pgp-signature",
".spl" => "application/futuresplash",
".class" => "application/octet-stream",
".ps" => "application/postscript",
".torrent" => "application/x-bittorrent",
".dvi" => "application/x-dvi",
".gz" => "application/x-gzip",
".pac" => "application/x-ns-proxy-autoconfig",
".swf" => "application/x-shockwave-flash",
".tar.gz" => "application/x-tgz",
".tgz" => "application/x-tgz",
".tar" => "application/x-tar",
".zip" => "application/zip",
".mp3" => "audio/mpeg",
".m3u" => "audio/x-mpegurl",
".wma" => "audio/x-ms-wma",
".wax" => "audio/x-ms-wax",
".ogg" => "audio/x-wav",
".wav" => "audio/x-wav",
".gif" => "image/gif",
".jpg" => "image/jpeg",
".jpeg" => "image/jpeg",
".png" => "image/png",
".xbm" => "image/x-xbitmap",
".xpm" => "image/x-xpixmap",
".xwd" => "image/x-xwindowdump",
".css" => "text/css",
".html" => "text/html",
".htm" => "text/html",
".js" => "text/javascript",
".asc" => "text/plain",
".c" => "text/plain",
".conf" => "text/plain",
".text" => "text/plain",
".txt" => "text/plain",
".dtd" => "text/xml",
".xml" => "text/xml",
".mpeg" => "video/mpeg",
".mpg" => "video/mpeg",
".mov" => "video/quicktime",
".qt" => "video/quicktime",
".avi" => "video/x-msvideo",
".asf" => "video/x-ms-asf",
".asx" => "video/x-ms-asf",
".wmv" => "video/x-ms-wmv",
".bz2" => "application/x-bzip",
".tbz" => "application/x-bzip-compressed-tar",
".tar.bz2" => "application/x-bzip-compressed-tar"
)

23
config/routes.rb Normal file
View file

@ -0,0 +1,23 @@
ActionController::Routing::Routes.draw do |map|
# Add your own custom routes here.
# The priority is based upon order of creation: first created -> highest priority.
# Here's a sample route:
# map.connect 'products/:id', :controller => 'catalog', :action => 'view'
# Keep in mind you can assign values other than :controller and :action
map.connect '', :controller=>'webmail/webmail', :action=>'index'
map.connect 'webmail', :controller=>'webmail/webmail', :action=>'index'
map.connect 'webmail/:action', :controller=>'webmail/webmail'
map.connect 'contacts/contact/:action', :controller=>'contacts/contact'
map.connect 'admin/main', :controller=> 'login', :action=>'logout'
# Allow downloading Web Service WSDL as a file with an extension
# instead of a file named 'wsdl'
map.connect ':controller/service.wsdl', :action => 'wsdl'
# Install the default route as the lowest priority.
map.connect ':controller/:action/:id'
end

111
db/schema.mysql.sql Normal file
View file

@ -0,0 +1,111 @@
CREATE TABLE customers (
id bigint(20) NOT NULL auto_increment,
fname varchar(50) default NULL,
lname varchar(50) default NULL,
email varchar(100) NOT NULL,
PRIMARY KEY (id),
UNIQUE INDEX (email)
) TYPE=MyISAM;
CREATE TABLE filters (
id bigint(20) NOT NULL auto_increment,
name varchar(50) default NULL,
destination_folder varchar(50) default NULL,
customer_id bigint(20) NOT NULL,
order_num int default 1,
PRIMARY KEY (id),
INDEX (customer_id),
FOREIGN KEY (customer_id) REFERENCES customers(id)
) TYPE=MyISAM;
CREATE TABLE expressions (
id bigint(20) NOT NULL auto_increment,
field_name varchar(20) default '^Subject' NOT NULL,
operator varchar(20) default 'contains' NOT NULL,
expr_value varchar(100) default '' NOT NULL,
case_sensitive bool default 0,
filter_id bigint(20) NOT NULL,
PRIMARY KEY (id),
INDEX (filter_id),
FOREIGN KEY (filter_id) REFERENCES filters(id)
) TYPE=MyISAM;
CREATE TABLE `mail_prefs` (
`id` int(11) NOT NULL auto_increment,
`mail_type` varchar(10) default 'text/plain',
`wm_rows` int(11) default '20',
`customer_id` bigint(20) default NULL,
`check_external_mail` tinyint(1) default '0',
PRIMARY KEY (`id`),
UNIQUE KEY `customer_id` (`customer_id`)
) TYPE=MyISAM;
CREATE TABLE contacts (
id bigint(20) NOT NULL auto_increment,
fname varchar(50) default NULL,
lname varchar(50) default NULL,
email varchar(100) default NULL,
hphone varchar(20) default NULL,
wphone varchar(20) default NULL,
mobile varchar(20) default NULL,
fax varchar(20) default NULL,
notes text,
create_date datetime default NULL,
delete_date datetime default NULL,
customer_id bigint(20) default NULL,
PRIMARY KEY (id),
INDEX (customer_id),
INDEX (customer_id, email),
INDEX (email),
FOREIGN KEY (customer_id) REFERENCES customers(id)
) TYPE=MyISAM;
CREATE TABLE contact_groups (
id bigint(20) NOT NULL auto_increment,
name varchar(50) default NULL,
customer_id bigint(20) default NULL,
PRIMARY KEY (id),
INDEX (customer_id),
FOREIGN KEY (customer_id) REFERENCES customers(id)
) TYPE=MyISAM;
CREATE TABLE contact_contact_groups (
contact_id bigint(20) NOT NULL,
contact_group_id bigint(20) NOT NULL,
PRIMARY KEY (contact_id, contact_group_id),
INDEX (contact_id),
INDEX (contact_group_id),
FOREIGN KEY (contact_id) REFERENCES contacts(id),
FOREIGN KEY (contact_group_id) REFERENCES contact_groups(id)
) TYPE=MyISAM;
-- Mysql Sessions
create table sessions (
id bigint(20) NOT NULL auto_increment,
session_id varchar(255),
data text,
updated_at timestamp,
primary key(id),
index(session_id)
);
-- Cache
CREATE TABLE imap_messages (
id bigint(20) NOT NULL auto_increment,
folder_name varchar(100) NOT NULL,
username varchar(100) NOT NULL,
msg_id varchar(100),
uid bigint(20) NOT NULL,
`from` varchar(255),
`from_flat` varchar(255),
`to` varchar(255),
`to_flat` varchar(255),
`subject` varchar(255),
`content_type` varchar(30),
`date` timestamp,
`unread` tinyint(1),
`size` bigint(20),
PRIMARY KEY (id),
INDEX (folder_name, username),
INDEX (folder_name, username,uid)
) TYPE=MyISAM;

111
db/schema.pgsql.sql Normal file
View file

@ -0,0 +1,111 @@
CREATE TABLE customers (
id bigserial NOT NULL,
fname varchar(50) default NULL,
lname varchar(50) default NULL,
email varchar(100) NOT NULL,
PRIMARY KEY (id)
);
CREATE UNIQUE INDEX customers_email_idx ON customers(email);
CREATE TABLE filters (
id bigserial NOT NULL,
name varchar(50) default NULL,
destination_folder varchar(50) default NULL,
customer_id bigint NOT NULL,
order_num int default 1,
PRIMARY KEY (id),
FOREIGN KEY (customer_id) REFERENCES customers(id)
);
CREATE INDEX filters_customer_id_idx ON filters(customer_id);
CREATE TABLE expressions (
id bigserial NOT NULL,
field_name varchar(20) default '^Subject' NOT NULL,
operator varchar(20) default 'contains' NOT NULL,
expr_value varchar(100) default '' NOT NULL,
case_sensitive bool default FALSE,
filter_id bigint NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (filter_id) REFERENCES filters(id)
);
CREATE INDEX expressions_filter_id_idx ON expressions(filter_id);
CREATE TABLE mail_prefs (
id serial NOT NULL,
mail_type varchar(10) default 'text/plain',
wm_rows int default '20',
customer_id bigint default NULL,
check_external_mail bool default false,
PRIMARY KEY (id)
);
CREATE UNIQUE INDEX mail_prefs_customer_id_idx ON mail_prefs(customer_id);
CREATE TABLE contacts (
id bigserial NOT NULL,
fname varchar(50) default NULL,
lname varchar(50) default NULL,
email varchar(100) default NULL,
hphone varchar(20) default NULL,
wphone varchar(20) default NULL,
mobile varchar(20) default NULL,
fax varchar(20) default NULL,
notes text,
create_date timestamp default NULL,
delete_date timestamp default NULL,
customer_id bigint default NULL,
PRIMARY KEY (id),
FOREIGN KEY (customer_id) REFERENCES customers(id)
);
CREATE INDEX contacts_customer_id_idx ON contacts(customer_id);
CREATE INDEX contacts_customer_id_email_idx ON contacts(customer_id,email);
CREATE INDEX contacts_email_idx ON contacts(email);
CREATE TABLE contact_groups (
id bigserial NOT NULL,
name varchar(50) default NULL,
customer_id bigint default NULL,
PRIMARY KEY (id),
FOREIGN KEY (customer_id) REFERENCES customers(id)
);
CREATE INDEX contact_groups_customer_id_idx ON contact_groups(customer_id);
CREATE TABLE contact_contact_groups (
contact_id bigint NOT NULL,
contact_group_id bigint NOT NULL,
PRIMARY KEY (contact_id, contact_group_id),
FOREIGN KEY (contact_id) REFERENCES contacts(id),
FOREIGN KEY (contact_group_id) REFERENCES contact_groups(id)
);
CREATE INDEX contact_contact_groups_contact_id_idx ON contact_contact_groups(contact_id);
CREATE INDEX contact_contact_groups_contact_group_id_idx ON contact_contact_groups(contact_group_id);
create table sessions (
id BIGSERIAL NOT NULL,
session_id VARCHAR(255) NULL,
data TEXT NULL,
updated_at TIMESTAMP default null,
PRIMARY KEY (id)
);
CREATE INDEX session_idx ON sessions(session_id);
CREATE TABLE imap_messages (
id BIGSERIAL NOT NULL,
folder_name VARCHAR(100) NOT NULL,
username VARCHAR(100) NOT NULL,
msg_id VARCHAR(100),
uid BIGINT NOT NULL,
"from" TEXT,
"from_flat" TEXT,
"to" TEXT,
"to_flat" TEXT,
"subject" TEXT,
"content_type" VARCHAR(30),
"date" TIMESTAMP,
"unread" BOOL default false,
"size" BIGINT,
PRIMARY KEY (id)
);
CREATE INDEX msg_cache_fu_idx ON imap_messages(folder_name, username);
CREATE INDEX msg_cache_fuui_idx ON imap_messages(folder_name, username, uid);

2
doc/README_FOR_APP Normal file
View file

@ -0,0 +1,2 @@
Use this README file to introduce your application and point to useful places in the API for learning more.
Run "rake appdoc" to generate API documentation for your models and controllers.

183
lib/cdfutils.rb Normal file
View file

@ -0,0 +1,183 @@
MIME_ENCODED = /=\?([a-z\-0-9]*)\?[QB]\?([a-zA-Z0-9+\/=\_\-]+)\?=/i
IMAP_EMAIL_ENVELOPE_FORMAT = /([a-zA-Z\-\.\_]*@[a-zA-Z\-\.\_]*)/
IMAP_EMAIL_ENVELOPE_FORMAT2 = /(.*)<([a-zA-Z\-\.\_]*@[a-zA-Z\-\.\_]*)>/
require 'iconv'
def valid_email?(email)
email.size < 100 && email =~ /.@.+\../ && email.count('@') == 1
end
def mime_encoded?( str )
return false if str.nil?
not (MIME_ENCODED =~ str).nil?
end
def from_qp(str, remove_underscore = true)
return '' if str.nil?
result = str.gsub(/=\r\n/, "")
result = result.gsub(/_/, " ") if remove_underscore
result.gsub!(/\r\n/m, $/)
result.gsub!(/=([\da-fA-F]{2})/) { $1.hex.chr }
result
end
def mime_decode(str, remove_underscore = true)
return '' if str.nil?
str.gsub(MIME_ENCODED) {|s|
enc = s.scan(MIME_ENCODED).flatten
if /\?Q\?/i =~ s
begin
Iconv.conv("UTF-8", enc[0], from_qp(enc[1], remove_underscore))
rescue
from_qp(enc[1], remove_underscore)
end
else
begin
Iconv.conv("UTF-8", enc[0], enc[1].unpack("m*").to_s)
rescue
enc[1].unpack("m*").to_s
end
end
}
end
def imap2friendlly_email(str)
begin
if str === IMAP_EMAIL_ENVELOPE_FORMAT
email = str.scan(IMAP_EMAIL_ENVELOPE_FORMAT)[0][0]
else
email = str.scan(IMAP_EMAIL_ENVELOPE_FORMAT2)[0][0]
end
name = str.slice(0, str.rindex(email)-1)
name = decode(name).to_s if mime_encoded?(name)
return "#{name.nil? ? '' : name.strip}<#{email}>"
rescue
"Error parsing str - #{str.scan(IMAP_EMAIL_ENVELOPE_FORMAT)} - #{str.scan(IMAP_EMAIL_ENVELOPE_FORMAT2)}"
end
end
def imap2friendlly_name(str)
begin
email = str.scan(IMAP_EMAIL_ENVELOPE_FORMAT)[0][0]
name = str.slice(0, str.rindex(email))
if name.nil? or name.strip == ""
return email
else
return name
end
rescue
str
end
end
def imap2friendlly_full_name(str)
begin
email = str.scan(IMAP_EMAIL_ENVELOPE_FORMAT)[0][0]
name = str.slice(0, str.rindex(email))
if name.nil? or name.strip == ""
return email
else
return "#{name}<#{email}>"
end
rescue
str
end
end
def imap2name_only(str)
email = str.scan(IMAP_EMAIL_ENVELOPE_FORMAT)[0][0]
name = str.slice(0, str.rindex(email))
return "#{name.nil? ? '' : name.strip}"
end
def imap2time(str)
begin
vals = str.scan(/(...), (.?.) (...) (....) (..):(..):(..) (.*)/)[0]
Time.local(vals[3],vals[2],vals[1],vals[4],vals[5],vals[6])
rescue
Time.now
end
end
def encode_email(names, email)
nameen = ""
names.each_byte { | ch | nameen = nameen +"=" + sprintf("%X",ch) }
return "=?#{CDF::CONFIG[:mail_charset]}?Q?#{nameen}?= <#{email}>"
end
# #############################
# HTML utils
# #############################
def replace_tag(tag, attrs)
replacements = {"body" => "",
"/body" => "",
"meta" => "",
"/meta" => "",
"head" => "",
"/head" => "",
"html" => "",
"/html" => "",
"title" => "<div class='notviscode'>",
"/title" => "</div>",
"div" => "",
"/div" => "",
"span" => "",
"/span" => "",
"layer" => "",
"/layer" => "",
"br" => "<br/>",
"/br" => "<br/>",
"iframe" => "",
"/iframe" => "",
"link" => "<xlink" << replace_attr(attrs) << ">",
"/link" => "</xlink" << replace_attr(attrs) << ">",
"style" => "<div class='notviscode'>",
"/style" => "</div>",
"script" => "<div class='notviscode'>",
"/script" => "</div>" }
replacements.fetch(tag.downcase, ("<" << tag.downcase << replace_attr(attrs) << ">"))
end
def replace_attr(attrs)
if attrs
attrs.downcase.gsub("onload", "onfilter").
gsub("onclick", "onfilter").
gsub("onkeypress", "onfilter").
gsub("javascript", "_javascript").
gsub("JavaScript", "_javascript")
else
""
end
end
def clear_html(text)
attribute_key = /[\w:_-]+/
attribute_value = /(?:[A-Za-z0-9\-_#\%\.,\/\:]+|(?:'[^']*?'|"[^"]*?"))/
attribute = /(?:#{attribute_key}(?:\s*=\s*#{attribute_value})?)/
attributes = /(?:#{attribute}(?:\s+#{attribute})*)/
tag_key = attribute_key
tag = %r{<([!/?\[]?(?:#{tag_key}|--))((?:\s+#{attributes})?\s*(?:[!/?\]]+|--)?)>}
text.gsub(tag, '').gsub(/\s+/, ' ').strip
CGI::escape(text)
end
def strip_html(text)
attribute_key = /[\w:_-]+/
attribute_value = /(?:[A-Za-z0-9\-_#\%\.,\/\:]+|(?:'[^']*?'|"[^"]*?"))/
attribute = /(?:#{attribute_key}(?:\s*=\s*#{attribute_value})?)/
attributes = /(?:#{attribute}(?:\s+#{attribute})*)/
tag_key = attribute_key
tag = %r{<([!/?\[]?(?:#{tag_key}|--))((?:\s+#{attributes})?\s*(?:[!/?\]]+|--)?)>}
res = text.gsub(tag) { |match|
ret = ""
match.scan(tag) { |token|
ret << replace_tag(token[0], token[1])
}
ret
}
# remove doctype tags
xattributes = /(?:#{attribute_value}(?:\s+#{attribute_value})*)/
xtag = %r{<!#{tag_key}((?:\s+#{xattributes})?\s*(?:[!/?\]]+|--)?)>}
res.gsub(xtag, '')
end

11
lib/gettext_extension.rb Executable file
View file

@ -0,0 +1,11 @@
require 'gettext'
include GetText # we want to be able to translate everywhere
module GetText
# GetText has this behaviour that it saves the binding to the mo file
# on a file by file basis. Every ruby file gets its own binding. We have to
# override this behaviour because it doesn't make any sense for a web
# application. We want one message catalog for all our files.
module_function
def callersrc; '*' end
end

38
lib/tmail_patch.rb Normal file
View file

@ -0,0 +1,38 @@
module TMail
class Mail
def parse_header( f )
name = field = nil
unixfrom = nil
while line = f.gets
case line
when /\A[ \t]/ # continue from prev line
raise SyntaxError, 'mail is began by space' unless field
field << ' ' << line.strip
when /\A([^\: \t]+):\s*/ # new header line
add_hf name, field if field
name = $1
field = $' #.strip
when /\A\-*\s*\z/ # end of header
add_hf name, field if field
name = field = nil
break
when /\AFrom (\S+)/
unixfrom = $1
else
# treat as continue from previos
raise SyntaxError, 'mail is began by space' unless field
field << ' ' << line.strip
end
end
add_hf name, field if name
if unixfrom
add_hf 'Return-Path', "<#{unixfrom}>" unless @header['return-path']
end
end
end
end

466
locale/.messages.pot Normal file
View file

@ -0,0 +1,466 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2005-04-07 18:17+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
#: app/controllers/webmail_controller.rb:57 /tmp/erb-gettext18743.0:30
msgid "Add new folder"
msgstr ""
#: app/controllers/webmail_controller.rb:59 app/helpers/webmail_helper.rb:74 app/helpers/webmail_helper.rb:74
msgid "(Delete)"
msgstr ""
#: app/controllers/webmail_controller.rb:61
msgid "(Subscribe)"
msgstr ""
#: app/controllers/webmail_controller.rb:62
msgid "(Select)"
msgstr ""
#: app/controllers/webmail_controller.rb:79 /tmp/erb-gettext18743.0:23
msgid "copy"
msgstr ""
#: app/controllers/webmail_controller.rb:83 /tmp/erb-gettext18743.0:24
msgid "move"
msgstr ""
#: app/controllers/webmail_controller.rb:87 /tmp/erb-gettext18743.0:89 /tmp/erb-gettext18743.0:16
#: /tmp/erb-gettext18743.0:22
msgid "delete"
msgstr ""
#: app/controllers/webmail_controller.rb:91 /tmp/erb-gettext18743.0:25
msgid "mark read"
msgstr ""
#: app/controllers/webmail_controller.rb:98 /tmp/erb-gettext18743.0:26
msgid "mark unread"
msgstr ""
#: app/controllers/webmail_controller.rb:110 /tmp/erb-gettext18743.0:17 /tmp/erb-gettext18743.0:8
msgid "Search"
msgstr ""
#: app/controllers/webmail_controller.rb:115 /tmp/erb-gettext18743.0:18 /tmp/erb-gettext18743.0:9
msgid "Show all"
msgstr ""
#: app/controllers/webmail_controller.rb:143
msgid "Password changed successfully"
msgstr ""
#: app/controllers/webmail_controller.rb:146
msgid "Password not changed - please enter correct password."
msgstr ""
#: app/controllers/webmail_controller.rb:181 /tmp/erb-gettext18743.0:18
msgid "Send"
msgstr ""
#: app/controllers/webmail_controller.rb:190 app/controllers/webmail_controller.rb:245
#: /tmp/erb-gettext18743.0:23 /tmp/erb-gettext18743.0:46 /tmp/erb-gettext18743.0:27
msgid "Add"
msgstr ""
#: app/controllers/webmail_controller.rb:228 app/controllers/webmail_controller.rb:247
#: /tmp/erb-gettext18743.0:51 /tmp/erb-gettext18743.0:73 /tmp/erb-gettext18743.0:18
#: /tmp/erb-gettext18743.0:33 /tmp/erb-gettext18743.0:35
msgid "Save"
msgstr ""
#: app/controllers/login_controller.rb:26
msgid "Wrong email or password specified."
msgstr ""
#: app/controllers/login_controller.rb:33
msgid "User successfully logged out"
msgstr ""
#: app/controllers/contact_controller.rb:135
msgid "You havent selected file or the file is empty"
msgstr ""
#: app/controllers/contact_controller.rb:161 app/controllers/contact_controller.rb:189
msgid "Contact %"
msgstr ""
#: app/controllers/contact_controller.rb:161 app/controllers/contact_controller.rb:189
msgid "The e-mail duplicates the e-mail of another record!"
msgstr ""
#: app/models/contact.rb:47
msgid "Please enter your first name (2 to 20 characters)."
msgstr ""
#: app/models/contact.rb:48
msgid "Please enter your surname (2 to 20 characters)."
msgstr ""
#: app/models/contact.rb:53
msgid "Contacts email cannot be changed."
msgstr ""
#: app/models/contact.rb:59
msgid "Please enter a valid email address."
msgstr ""
#: app/models/contact.rb:63
msgid "An account for your email address already exists."
msgstr ""
#: app/models/contact_group.rb:15
msgid "Please enter group name (1 to 50 characters)"
msgstr ""
#: app/models/contact_group.rb:21
msgid "You already have contact group with this name"
msgstr ""
#: app/helpers/webmail_helper.rb:6
msgid "Folders"
msgstr ""
#: app/helpers/webmail_helper.rb:10
msgid "Compose"
msgstr ""
#: app/helpers/webmail_helper.rb:14
msgid "Refresh"
msgstr ""
#: app/helpers/webmail_helper.rb:18
msgid "Message list"
msgstr ""
#: app/helpers/webmail_helper.rb:22
msgid "Reply"
msgstr ""
#: app/helpers/webmail_helper.rb:26
msgid "Forward"
msgstr ""
#: app/helpers/webmail_helper.rb:30 app/helpers/webmail_helper.rb:123
msgid "Delete"
msgstr ""
#: app/helpers/webmail_helper.rb:34
msgid "View source"
msgstr ""
#: app/helpers/webmail_helper.rb:38
msgid "Manage folders"
msgstr ""
#: app/helpers/webmail_helper.rb:42 /tmp/erb-gettext18743.0:1
msgid "Change password"
msgstr ""
#: app/helpers/webmail_helper.rb:46
msgid "Preferences"
msgstr ""
#: app/helpers/webmail_helper.rb:50
msgid "Mail Filters"
msgstr ""
#: app/helpers/webmail_helper.rb:54 /tmp/erb-gettext18743.0:1 /tmp/erb-gettext18743.0:19
msgid "Contacts"
msgstr ""
#: app/helpers/webmail_helper.rb:58
msgid "Logout"
msgstr ""
#: app/helpers/webmail_helper.rb:63
msgid "(Empty)"
msgstr ""
#: app/helpers/webmail_helper.rb:111
msgid "Up"
msgstr ""
#: app/helpers/webmail_helper.rb:115
msgid "Down"
msgstr ""
#: app/helpers/webmail_helper.rb:119
msgid "Edit"
msgstr ""
#: app/helpers/application_helper.rb:156
msgid "First"
msgstr ""
#: app/helpers/application_helper.rb:157
msgid "Prev"
msgstr ""
#: app/helpers/application_helper.rb:158
msgid "Next"
msgstr ""
#: app/helpers/application_helper.rb:159
msgid "Last"
msgstr ""
#: app/views/contact/add.rhtml:1
msgid "Edit/Create contact"
msgstr ""
#: app/views/contact/add.rhtml:13 /tmp/erb-gettext18743.0:17 /tmp/erb-gettext18743.0:17
msgid "First name"
msgstr ""
#: app/views/contact/add.rhtml:14 /tmp/erb-gettext18743.0:18 /tmp/erb-gettext18743.0:18
msgid "Last name"
msgstr ""
#: app/views/contact/add.rhtml:15 /tmp/erb-gettext18743.0:19 /tmp/erb-gettext18743.0:28
#: /tmp/erb-gettext18743.0:59 /tmp/erb-gettext18743.0:80 /tmp/erb-gettext18743.0:14
msgid "E-mail"
msgstr ""
#: app/views/contact/add.rhtml:22
msgid "Contact belong to these groups"
msgstr ""
#: app/views/contact/add.rhtml:52
msgid "Save and add another"
msgstr ""
#: app/views/contact/add.rhtml:53 /tmp/erb-gettext18743.0:38 /tmp/erb-gettext18743.0:21
msgid "Back to contacts"
msgstr ""
#: app/views/contact/add.rhtml:55 /tmp/erb-gettext18743.0:99
msgid "Back to message"
msgstr ""
#: app/views/contact/import_preview.rhtml:1
msgid "Contacts You Are About To Import"
msgstr ""
#: app/views/contact/import_preview.rhtml:4 /tmp/erb-gettext18743.0:3
msgid "Errors"
msgstr ""
#: app/views/contact/import_preview.rhtml:36 /tmp/erb-gettext18743.0:20
msgid "Import"
msgstr ""
#: app/views/contact/import_preview.rhtml:37
msgid "Choose another file"
msgstr ""
#: app/views/contact/import_preview.rhtml:39 app/views/contact/list.rhtml:96 /tmp/erb-gettext18743.0:22
#: /tmp/erb-gettext18743.0:22
msgid "Back to folders"
msgstr ""
#: app/views/contact/list.rhtml:26
msgid "To&nbsp;CC&nbsp;BCC"
msgstr ""
#: app/views/contact/list.rhtml:27 app/views/contact/list.rhtml:58 app/views/contact/list.rhtml:79
#: /tmp/erb-gettext18743.0:7 /tmp/erb-gettext18743.0:13
msgid "Name"
msgstr ""
#: app/views/contact/list.rhtml:39 app/views/contact/list.rhtml:97
msgid "Groups"
msgstr ""
#: app/views/contact/list.rhtml:51
msgid "choose"
msgstr ""
#: app/views/contact/list.rhtml:52
msgid "cancel"
msgstr ""
#: app/views/contact/list.rhtml:74 /tmp/erb-gettext18743.0:19
msgid "Back to groups"
msgstr ""
#: app/views/contact/list.rhtml:89
msgid "DELETE CONTACT?\r\name - %s\r\nE-mail - %s"
msgstr ""
#: app/views/contact/list.rhtml:94
msgid "Add One Contact"
msgstr ""
#: app/views/contact/list.rhtml:95
msgid "Add Multiple Contacts"
msgstr ""
#: app/views/contact/add_multiple.rhtml:1
msgid "Add multiple contacts"
msgstr ""
#: app/views/contact/add_multiple.rhtml:11
msgid "Comma-separated (CSV) file"
msgstr ""
#: app/views/contact/add_multiple.rhtml:12
msgid "Tab-delimited text file"
msgstr ""
#: app/views/contact/add_multiple.rhtml:15
msgid "Select file"
msgstr ""
#: app/views/login/index.rhtml:20 app/views/login/index.rhtml:20
msgid "Email"
msgstr ""
#: app/views/login/index.rhtml:24 app/views/login/index.rhtml:24 /tmp/erb-gettext18743.0:16
msgid "Password"
msgstr ""
#: app/views/login/index.rhtml:29
msgid "Log In"
msgstr ""
#: app/views/layouts/public.rhtml:5 /tmp/erb-gettext18743.0:5
msgid "Mailr"
msgstr ""
#: app/views/contact_group/list.rhtml:1
msgid "Contact Groups"
msgstr ""
#: app/views/contact_group/list.rhtml:14
msgid "members"
msgstr ""
#: app/views/contact_group/list.rhtml:15
msgid "edit"
msgstr ""
#: app/views/contact_group/list.rhtml:16
msgid "DELETE CONTACT GROUP %s?"
msgstr ""
#: app/views/contact_group/list.rhtml:21
msgid "Add Contact Group"
msgstr ""
#: app/views/contact_group/edit.rhtml:1
msgid "Edit/Create Contact Group"
msgstr ""
#: app/views/webmail/filter.rhtml:1 /tmp/erb-gettext18743.0:1 /tmp/erb-gettext18743.0:1
#: /tmp/erb-gettext18743.0:1 /tmp/erb-gettext18743.0:1 /tmp/erb-gettext18743.0:1
#: /tmp/erb-gettext18743.0:1 /tmp/erb-gettext18743.0:1 /tmp/erb-gettext18743.0:1
msgid "Mailbox of (%s)"
msgstr ""
#: app/views/webmail/filter.rhtml:18 /tmp/erb-gettext18743.0:18
msgid "Filter name"
msgstr ""
#: app/views/webmail/filter.rhtml:21
msgid "Messages matching"
msgstr ""
#: app/views/webmail/filter.rhtml:25
msgid "Will be placed in"
msgstr ""
#: app/views/webmail/filter.rhtml:34 /tmp/erb-gettext18743.0:36
msgid "Cancel"
msgstr ""
#: app/views/webmail/manage_folders.rhtml:15
msgid "Folder list"
msgstr ""
#: app/views/webmail/manage_folders.rhtml:30
msgid "Folder name"
msgstr ""
#: app/views/webmail/messages.rhtml:20
msgid "Operations on marked messages"
msgstr ""
#: app/views/webmail/messages.rhtml:29
msgid "Destination for move and copy operations"
msgstr ""
#: app/views/webmail/messages.rhtml:44 /tmp/erb-gettext18743.0:16 /tmp/erb-gettext18743.0:21
msgid "To"
msgstr ""
#: app/views/webmail/messages.rhtml:46
msgid "From"
msgstr ""
#: app/views/webmail/messages.rhtml:48 /tmp/erb-gettext18743.0:23 /tmp/erb-gettext18743.0:24
msgid "Subject"
msgstr ""
#: app/views/webmail/messages.rhtml:49
msgid "Date"
msgstr ""
#: app/views/webmail/change_password.rhtml:18
msgid "New password"
msgstr ""
#: app/views/webmail/change_password.rhtml:20
msgid "Change"
msgstr ""
#: app/views/webmail/folders.rhtml:16
msgid "Mailbox folders"
msgstr ""
#: app/views/webmail/prefs.rhtml:20
msgid "Send type message"
msgstr ""
#: app/views/webmail/prefs.rhtml:27
msgid "Messages per page"
msgstr ""
#: app/views/webmail/mailsent.rhtml:18 /tmp/erb-gettext18743.0:22
msgid "CC"
msgstr ""
#: app/views/webmail/mailsent.rhtml:21 /tmp/erb-gettext18743.0:23
msgid "BCC"
msgstr ""
#: app/views/webmail/_search.rhtml:2
msgid "Search in message field"
msgstr ""
#: app/views/webmail/_search.rhtml:6
msgid "for"
msgstr ""
#: app/views/webmail/compose.rhtml:45
msgid "Attachment"
msgstr ""
#: app/views/webmail/_expr.rhtml:15
msgid "case sensitive"
msgstr ""

Binary file not shown.

View file

@ -0,0 +1,527 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: Mailr 1.0\n"
"POT-Creation-Date: 2005-04-07 18:17+0300\n"
"PO-Revision-Date: 2005-04-07 18:39+0200\n"
"Last-Translator: Nick Penkov <nick dot penkov at gmail dot com>\n"
"Language-Team: bg <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Poedit-Language: Bulgarian\n"
"X-Poedit-Country: BULGARIA\n"
"X-Poedit-SourceCharset: iso-8859-1\n"
"X-Poedit-Basepath: /projects/mailr\n"
#: app/controllers/webmail_controller.rb:57
#: /tmp/erb-gettext18743.0:30
msgid "Add new folder"
msgstr "Добавяне на папка"
#: app/controllers/webmail_controller.rb:59
#: app/helpers/webmail_helper.rb:74
msgid "(Delete)"
msgstr "(Изтриване)"
#: app/controllers/webmail_controller.rb:61
msgid "(Subscribe)"
msgstr "(Абонирне)"
#: app/controllers/webmail_controller.rb:62
msgid "(Select)"
msgstr "(Избиране)"
#: app/controllers/webmail_controller.rb:79
#: /tmp/erb-gettext18743.0:23
msgid "copy"
msgstr "копирай"
#: app/controllers/webmail_controller.rb:83
#: /tmp/erb-gettext18743.0:24
msgid "move"
msgstr "премести"
#: app/controllers/webmail_controller.rb:87
#: /tmp/erb-gettext18743.0:89
#: /tmp/erb-gettext18743.0:16
#: /tmp/erb-gettext18743.0:22
msgid "delete"
msgstr "изтриий"
#: app/controllers/webmail_controller.rb:91
#: /tmp/erb-gettext18743.0:25
msgid "mark read"
msgstr "прочетено"
#: app/controllers/webmail_controller.rb:98
#: /tmp/erb-gettext18743.0:26
msgid "mark unread"
msgstr "непрочетено"
#: app/controllers/webmail_controller.rb:110
#: /tmp/erb-gettext18743.0:17
#: /tmp/erb-gettext18743.0:8
msgid "Search"
msgstr "Търси"
#: app/controllers/webmail_controller.rb:115
#: /tmp/erb-gettext18743.0:18
#: /tmp/erb-gettext18743.0:9
msgid "Show all"
msgstr "Покажи всички"
#: app/controllers/webmail_controller.rb:143
msgid "Password changed successfully"
msgstr "Паролата е сменена успешно."
#: app/controllers/webmail_controller.rb:146
msgid "Password not changed - please enter correct password."
msgstr "Паролата не е сменена - моля въведете коректна парола."
#: app/controllers/webmail_controller.rb:181
#: /tmp/erb-gettext18743.0:18
msgid "Send"
msgstr "Изпрати"
#: app/controllers/webmail_controller.rb:190
#: app/controllers/webmail_controller.rb:245
#: /tmp/erb-gettext18743.0:23
#: /tmp/erb-gettext18743.0:46
#: /tmp/erb-gettext18743.0:27
msgid "Add"
msgstr "Добави"
#: app/controllers/webmail_controller.rb:228
#: app/controllers/webmail_controller.rb:247
#: /tmp/erb-gettext18743.0:51
#: /tmp/erb-gettext18743.0:73
#: /tmp/erb-gettext18743.0:18
#: /tmp/erb-gettext18743.0:33
#: /tmp/erb-gettext18743.0:35
msgid "Save"
msgstr "Съхрани"
#: app/controllers/login_controller.rb:26
msgid "Wrong email or password specified."
msgstr "Грешен e-mail или парола са въведени."
#: app/controllers/login_controller.rb:33
msgid "User successfully logged out"
msgstr "Потребителя успешно излезе от системата."
#: app/controllers/contact_controller.rb:135
msgid "You havent selected file or the file is empty"
msgstr "Не сте избрали файл или файлът е празен."
#: app/controllers/contact_controller.rb:161
#: app/controllers/contact_controller.rb:189
msgid "Contact %"
msgstr "Адрес %"
#: app/controllers/contact_controller.rb:161
#: app/controllers/contact_controller.rb:189
msgid "The e-mail duplicates the e-mail of another record!"
msgstr "The e-mail duplicates the e-mail of another record!"
#: app/models/contact.rb:47
msgid "Please enter your first name (2 to 20 characters)."
msgstr "Please enter your first name (2 to 20 characters)."
#: app/models/contact.rb:48
msgid "Please enter your surname (2 to 20 characters)."
msgstr "Please enter your surname (2 to 20 characters)."
#: app/models/contact.rb:53
msgid "Contacts email cannot be changed."
msgstr "Contacts email cannot be changed."
#: app/models/contact.rb:59
msgid "Please enter a valid email address."
msgstr "Please enter a valid email address."
#: app/models/contact.rb:63
msgid "An account for your email address already exists."
msgstr "An account for your email address already exists."
#: app/models/contact_group.rb:15
msgid "Please enter group name (1 to 50 characters)"
msgstr "Please enter group name (1 to 50 characters)"
#: app/models/contact_group.rb:21
msgid "You already have contact group with this name"
msgstr "You already have contact group with this name"
#: app/helpers/webmail_helper.rb:6
msgid "Folders"
msgstr "Папки"
#: app/helpers/webmail_helper.rb:10
msgid "Compose"
msgstr "Ново писмо"
#: app/helpers/webmail_helper.rb:14
msgid "Refresh"
msgstr "Refresh"
#: app/helpers/webmail_helper.rb:18
msgid "Message list"
msgstr "Списък съобщения"
#: app/helpers/webmail_helper.rb:22
msgid "Reply"
msgstr "Отговори"
#: app/helpers/webmail_helper.rb:26
msgid "Forward"
msgstr "Препрати"
#: app/helpers/webmail_helper.rb:30
#: app/helpers/webmail_helper.rb:123
msgid "Delete"
msgstr "Изтрий"
#: app/helpers/webmail_helper.rb:34
msgid "View source"
msgstr "Преглед на оригинала"
#: app/helpers/webmail_helper.rb:38
msgid "Manage folders"
msgstr "Управление на папки"
#: app/helpers/webmail_helper.rb:42
#: /tmp/erb-gettext18743.0:1
msgid "Change password"
msgstr "Смени парола"
#: app/helpers/webmail_helper.rb:46
msgid "Preferences"
msgstr "Настройки"
#: app/helpers/webmail_helper.rb:50
msgid "Mail Filters"
msgstr "Филтри"
#: app/helpers/webmail_helper.rb:54
#: /tmp/erb-gettext18743.0:1
#: /tmp/erb-gettext18743.0:19
msgid "Contacts"
msgstr "Адреси"
#: app/helpers/webmail_helper.rb:58
msgid "Logout"
msgstr "Изход"
#: app/helpers/webmail_helper.rb:63
msgid "(Empty)"
msgstr "(Empty)"
#: app/helpers/webmail_helper.rb:111
msgid "Up"
msgstr "Нагоре"
#: app/helpers/webmail_helper.rb:115
msgid "Down"
msgstr "Надолу"
#: app/helpers/webmail_helper.rb:119
msgid "Edit"
msgstr "Редактирай"
#: app/helpers/application_helper.rb:156
msgid "First"
msgstr "Първи"
#: app/helpers/application_helper.rb:157
msgid "Prev"
msgstr "Следващ"
#: app/helpers/application_helper.rb:158
msgid "Next"
msgstr "Предишен"
#: app/helpers/application_helper.rb:159
msgid "Last"
msgstr "Последен"
#: app/views/contact/add.rhtml:1
msgid "Edit/Create contact"
msgstr "Edit/Create contact"
#: app/views/contact/add.rhtml:13
#: /tmp/erb-gettext18743.0:17
msgid "First name"
msgstr "First name"
#: app/views/contact/add.rhtml:14
#: /tmp/erb-gettext18743.0:18
msgid "Last name"
msgstr "Last name"
#: app/views/contact/add.rhtml:15
#: /tmp/erb-gettext18743.0:19
#: /tmp/erb-gettext18743.0:28
#: /tmp/erb-gettext18743.0:59
#: /tmp/erb-gettext18743.0:80
#: /tmp/erb-gettext18743.0:14
msgid "E-mail"
msgstr "E-mail"
#: app/views/contact/add.rhtml:22
msgid "Contact belong to these groups"
msgstr "Contact belong to these groups"
#: app/views/contact/add.rhtml:52
msgid "Save and add another"
msgstr "Save and add another"
#: app/views/contact/add.rhtml:53
#: /tmp/erb-gettext18743.0:38
#: /tmp/erb-gettext18743.0:21
msgid "Back to contacts"
msgstr "Back to contacts"
#: app/views/contact/add.rhtml:55
#: /tmp/erb-gettext18743.0:99
msgid "Back to message"
msgstr "Back to message"
#: app/views/contact/import_preview.rhtml:1
msgid "Contacts You Are About To Import"
msgstr "Contacts You Are About To Import"
#: app/views/contact/import_preview.rhtml:4
#: /tmp/erb-gettext18743.0:3
msgid "Errors"
msgstr "Errors"
#: app/views/contact/import_preview.rhtml:36
#: /tmp/erb-gettext18743.0:20
msgid "Import"
msgstr "Import"
#: app/views/contact/import_preview.rhtml:37
msgid "Choose another file"
msgstr "Choose another file"
#: app/views/contact/import_preview.rhtml:39
#: app/views/contact/list.rhtml:96
#: /tmp/erb-gettext18743.0:22
msgid "Back to folders"
msgstr "Back to folders"
#: app/views/contact/list.rhtml:26
msgid "To&nbsp;CC&nbsp;BCC"
msgstr "To&nbsp;CC&nbsp;BCC"
#: app/views/contact/list.rhtml:27
#: app/views/contact/list.rhtml:58
#: app/views/contact/list.rhtml:79
#: /tmp/erb-gettext18743.0:7
#: /tmp/erb-gettext18743.0:13
msgid "Name"
msgstr "Name"
#: app/views/contact/list.rhtml:39
#: app/views/contact/list.rhtml:97
msgid "Groups"
msgstr "Groups"
#: app/views/contact/list.rhtml:51
msgid "choose"
msgstr "choose"
#: app/views/contact/list.rhtml:52
msgid "cancel"
msgstr "cancel"
#: app/views/contact/list.rhtml:74
#: /tmp/erb-gettext18743.0:19
msgid "Back to groups"
msgstr "Back to groups"
#: app/views/contact/list.rhtml:89
msgid ""
"DELETE CONTACT?\r\n"
"ame - %s\r\n"
"E-mail - %s"
msgstr ""
"DELETE CONTACT?\r\n"
"ame - %s\r\n"
"E-mail - %s"
#: app/views/contact/list.rhtml:94
msgid "Add One Contact"
msgstr "Add One Contact"
#: app/views/contact/list.rhtml:95
msgid "Add Multiple Contacts"
msgstr "Add Multiple Contacts"
#: app/views/contact/add_multiple.rhtml:1
msgid "Add multiple contacts"
msgstr "Add multiple contacts"
#: app/views/contact/add_multiple.rhtml:11
msgid "Comma-separated (CSV) file"
msgstr "Comma-separated (CSV) file"
#: app/views/contact/add_multiple.rhtml:12
msgid "Tab-delimited text file"
msgstr "Tab-delimited text file"
#: app/views/contact/add_multiple.rhtml:15
msgid "Select file"
msgstr "Select file"
#: app/views/login/index.rhtml:20
msgid "Email"
msgstr "Email"
#: app/views/login/index.rhtml:24
#: /tmp/erb-gettext18743.0:16
msgid "Password"
msgstr "Password"
#: app/views/login/index.rhtml:29
msgid "Log In"
msgstr "Log In"
#: app/views/layouts/public.rhtml:5
#: /tmp/erb-gettext18743.0:5
msgid "Mailr"
msgstr "Mailr"
#: app/views/contact_group/list.rhtml:1
msgid "Contact Groups"
msgstr "Contact Groups"
#: app/views/contact_group/list.rhtml:14
msgid "members"
msgstr "members"
#: app/views/contact_group/list.rhtml:15
msgid "edit"
msgstr "edit"
#: app/views/contact_group/list.rhtml:16
msgid "DELETE CONTACT GROUP %s?"
msgstr "DELETE CONTACT GROUP %s?"
#: app/views/contact_group/list.rhtml:21
msgid "Add Contact Group"
msgstr "Add Contact Group"
#: app/views/contact_group/edit.rhtml:1
msgid "Edit/Create Contact Group"
msgstr "Edit/Create Contact Group"
#: app/views/webmail/filter.rhtml:1
#: /tmp/erb-gettext18743.0:1
msgid "Mailbox of (%s)"
msgstr "Пощенска кутия на (%s)"
#: app/views/webmail/filter.rhtml:18
#: /tmp/erb-gettext18743.0:18
msgid "Filter name"
msgstr "Filter name"
#: app/views/webmail/filter.rhtml:21
msgid "Messages matching"
msgstr "Messages matching"
#: app/views/webmail/filter.rhtml:25
msgid "Will be placed in"
msgstr "Will be placed in"
#: app/views/webmail/filter.rhtml:34
#: /tmp/erb-gettext18743.0:36
msgid "Cancel"
msgstr "Cancel"
#: app/views/webmail/manage_folders.rhtml:15
msgid "Folder list"
msgstr "Folder list"
#: app/views/webmail/manage_folders.rhtml:30
msgid "Folder name"
msgstr "Folder name"
#: app/views/webmail/messages.rhtml:20
msgid "Operations on marked messages"
msgstr "Operations on marked messages"
#: app/views/webmail/messages.rhtml:29
msgid "Destination for move and copy operations"
msgstr "Destination for move and copy operations"
#: app/views/webmail/messages.rhtml:44
#: /tmp/erb-gettext18743.0:16
#: /tmp/erb-gettext18743.0:21
msgid "To"
msgstr "To"
#: app/views/webmail/messages.rhtml:46
msgid "From"
msgstr "From"
#: app/views/webmail/messages.rhtml:48
#: /tmp/erb-gettext18743.0:23
#: /tmp/erb-gettext18743.0:24
msgid "Subject"
msgstr "Subject"
#: app/views/webmail/messages.rhtml:49
msgid "Date"
msgstr "Date"
#: app/views/webmail/change_password.rhtml:18
msgid "New password"
msgstr "New password"
#: app/views/webmail/change_password.rhtml:20
msgid "Change"
msgstr "Change"
#: app/views/webmail/folders.rhtml:16
msgid "Mailbox folders"
msgstr "Mailbox folders"
#: app/views/webmail/prefs.rhtml:20
msgid "Send type message"
msgstr "Send type message"
#: app/views/webmail/prefs.rhtml:27
msgid "Messages per page"
msgstr "Messages per page"
#: app/views/webmail/mailsent.rhtml:18
#: /tmp/erb-gettext18743.0:22
msgid "CC"
msgstr "CC"
#: app/views/webmail/mailsent.rhtml:21
#: /tmp/erb-gettext18743.0:23
msgid "BCC"
msgstr "BCC"
#: app/views/webmail/_search.rhtml:2
msgid "Search in message field"
msgstr "Search in message field"
#: app/views/webmail/_search.rhtml:6
msgid "for"
msgstr "for"
#: app/views/webmail/compose.rhtml:45
msgid "Attachment"
msgstr "Attachment"
#: app/views/webmail/_expr.rhtml:15
msgid "case sensitive"
msgstr "case sensitive"

40
public/.htaccess Normal file
View file

@ -0,0 +1,40 @@
# General Apache options
AddHandler fastcgi-script .fcgi
AddHandler cgi-script .cgi
Options +FollowSymLinks +ExecCGI
# If you don't want Rails to look in certain directories,
# use the following rewrite rules so that Apache won't rewrite certain requests
#
# Example:
# RewriteCond %{REQUEST_URI} ^/notrails.*
# RewriteRule .* - [L]
# Redirect all requests not available on the filesystem to Rails
# By default the cgi dispatcher is used which is very slow
#
# For better performance replace the dispatcher with the fastcgi one
#
# Example:
# RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
RewriteEngine On
# If your Rails application is accessed via an Alias directive,
# then you MUST also set the RewriteBase in this htaccess file.
#
# Example:
# Alias /myrailsapp /path/to/myrailsapp/public
# RewriteBase /myrailsapp
RewriteRule ^$ /login [QSA]
RewriteRule ^([^.]+)$ $1.html [QSA]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ dispatch.cgi [QSA,L]
# In case Rails experiences terminal errors
# Instead of displaying this message you can supply a file here which will be rendered instead
#
# Example:
# ErrorDocument 500 /500.html
ErrorDocument 500 "<h2>Application error</h2>Rails application failed to start properly"

8
public/404.html Normal file
View file

@ -0,0 +1,8 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<body>
<h1>File not found</h1>
<p>Change this error message for pages not found in public/404.html</p>
</body>
</html>

8
public/500.html Normal file
View file

@ -0,0 +1,8 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<body>
<h1>Application error (Apache)</h1>
<p>Change this error message for exceptions thrown outside of an action (like in Dispatcher setups or broken Ruby code) in public/500.html</p>
</body>
</html>

10
public/dispatch.cgi Executable file
View file

@ -0,0 +1,10 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT)
# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like:
# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired
require "dispatcher"
ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun)
Dispatcher.dispatch

24
public/dispatch.fcgi Executable file
View file

@ -0,0 +1,24 @@
#!/usr/bin/env ruby
#
# You may specify the path to the FastCGI crash log (a log of unhandled
# exceptions which forced the FastCGI instance to exit, great for debugging)
# and the number of requests to process before running garbage collection.
#
# By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log
# and the GC period is nil (turned off). A reasonable number of requests
# could range from 10-100 depending on the memory footprint of your app.
#
# Example:
# # Default log path, normal GC behavior.
# RailsFCGIHandler.process!
#
# # Default log path, 50 requests between GC.
# RailsFCGIHandler.process! nil, 50
#
# # Custom log path, normal GC behavior.
# RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log'
#
require File.dirname(__FILE__) + "/../config/environment"
require 'fcgi_handler'
RailsFCGIHandler.process!

10
public/dispatch.rb Executable file
View file

@ -0,0 +1,10 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT)
# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like:
# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired
require "dispatcher"
ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun)
Dispatcher.dispatch

0
public/favicon.ico Normal file
View file

Binary file not shown.

After

Width:  |  Height:  |  Size: 657 B

BIN
public/images/d6deec.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 B

BIN
public/images/deselect.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 B

BIN
public/images/select.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

BIN
public/images/white.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 B

BIN
public/images/white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

78
public/index.html Normal file
View file

@ -0,0 +1,78 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Rails: Welcome on board</title>
<style>
body { background-color: #fff; color: #333; }
body, p, ol, ul, td {
font-family: verdana, arial, helvetica, sans-serif;
font-size: 12px;
line-height: 18px;
}
li {
margin-bottom: 7px;
}
pre {
background-color: #eee;
padding: 10px;
font-size: 11px;
}
a { color: #000; }
a:visited { color: #666; }
a:hover { color: #fff; background-color:#000; }
</style>
</head>
<body>
<h1>Congratulations, you've put Ruby on Rails!</h1>
<p><b>Before you move on</b>, verify that the following conditions have been met:</p>
<ol>
<li>The log and public directories must be writable to the web server (<code>chmod -R 775 log</code> and <code>chmod -R 775 public</code>).
<li>
The shebang line in the public/dispatch* files must reference your Ruby installation. <br/>
You might need to change it to <code>#!/usr/bin/env ruby</code> or point directly at the installation.
</li>
<li>
Rails on Apache needs to have the cgi handler and mod_rewrite enabled. <br/>
Somewhere in your httpd.conf, you should have:<br/>
<code>AddHandler cgi-script .cgi</code><br/>
<code>LoadModule rewrite_module libexec/httpd/mod_rewrite.so</code><br/>
<code>AddModule mod_rewrite.c</code>
</li>
</ol>
<p>Take the following steps to get started:</p>
<ol>
<li>Create empty development and test databases for your application.<br/>
<small>Recommendation: Use *_development and *_test names, such as basecamp_development and basecamp_test</small><br/>
<small>Warning: Don't point your test database at your development database, it'll destroy the latter on test runs!</small>
<li>Edit config/database.yml with your database settings.
<li>Create controllers and models using the generator in <code>script/generate</code> <br/>
<small>Help: Run the generator with no arguments for documentation</small>
<li>See all the tests run by running <code>rake</code>.
<li>Develop your Rails application!
<li>Setup Apache with <a href="http://www.fastcgi.com">FastCGI</a> (and <a href="http://raa.ruby-lang.org/list.rhtml?name=fcgi">Ruby bindings</a>), if you need better performance
<li>Remove the dispatches you don't use (so if you're on FastCGI, delete/move dispatch.rb, dispatch.cgi and gateway.cgi)</li>
</ol>
<p>
Trying to setup a default page for Rails using Routes? You'll have to delete this file (public/index.html) to get under way. Then define a new route in <tt>config/routes.rb</tt> of the form:
<pre> map.connect '', :controller => 'wiki/page', :action => 'show', :title => 'Welcome'</pre>
</p>
<p>
Having problems getting up and running? First try debugging it yourself by looking at the log files. <br/>
Then try the friendly Rails community <a href="http://www.rubyonrails.org">on the web</a> or <a href="http://www.rubyonrails.org/show/IRC">on IRC</a>
(<a href="irc://irc.freenode.net/#rubyonrails">FreeNode#rubyonrails</a>).
</p>
</body>
</html>

View file

@ -0,0 +1,52 @@
var fieldTo = ""
var fieldToc = ""
var fieldCC = ""
var fieldBCC = ""
function respondTo(str) {
if (fieldTo == "") fieldTo += str
else fieldTo += "," + str
}
function respondTo(str, contactId) {
if (fieldTo == "") fieldTo += str
else fieldTo += "," + str
if (fieldToc == "") fieldToc += contactId
else fieldToc += "," + contactId
}
function respondCC(str) {
if (fieldCC == "") fieldCC += str
else fieldCC += "," + str
}
function respondBCC(str) {
if (fieldBCC == "") fieldBCC += str
else fieldBCC += "," + str
}
function respondToCaller() {
if (window.opener) {
doc = window.opener.document;
setAddrField(getFormFieldPoint(doc, 'mail_to'), fieldTo);
setAddrField(getFormFieldPoint(doc, 'mail_toc'), fieldToc);
setAddrField(getFormFieldPoint(doc, 'mail_cc'), fieldCC);
setAddrField(getFormFieldPoint(doc, 'mail_bcc'), fieldBCC);
window.close();
}
}
function getFormFieldPoint(doc, id) {
if ( doc.getElementById ) elem = doc.getElementById( id );
else if ( doc.all ) elem = doc.eval( "document.all." + id );
return elem
}
function setAddrField(fld, value) {
if (value != "") {
if (fld.value == "") fld.value = value;
else fld.value += "," + value;
}
}

708
public/javascripts/controls.js vendored Normal file
View file

@ -0,0 +1,708 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
// (c) 2005 Jon Tirsen (http://www.tirsen.com)
// Contributors:
// Richard Livsey
// Rahul Bhargava
// Rob Wills
//
// See scriptaculous.js for full license.
// Autocompleter.Base handles all the autocompletion functionality
// that's independent of the data source for autocompletion. This
// includes drawing the autocompletion menu, observing keyboard
// and mouse events, and similar.
//
// Specific autocompleters need to provide, at the very least,
// a getUpdatedChoices function that will be invoked every time
// the text inside the monitored textbox changes. This method
// should get the text for which to provide autocompletion by
// invoking this.getToken(), NOT by directly accessing
// this.element.value. This is to allow incremental tokenized
// autocompletion. Specific auto-completion logic (AJAX, etc)
// belongs in getUpdatedChoices.
//
// Tokenized incremental autocompletion is enabled automatically
// when an autocompleter is instantiated with the 'tokens' option
// in the options parameter, e.g.:
// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
// will incrementally autocomplete with a comma as the token.
// Additionally, ',' in the above example can be replaced with
// a token array, e.g. { tokens: [',', '\n'] } which
// enables autocompletion on multiple tokens. This is most
// useful when one of the tokens is \n (a newline), as it
// allows smart autocompletion after linebreaks.
var Autocompleter = {}
Autocompleter.Base = function() {};
Autocompleter.Base.prototype = {
baseInitialize: function(element, update, options) {
this.element = $(element);
this.update = $(update);
this.hasFocus = false;
this.changed = false;
this.active = false;
this.index = 0;
this.entryCount = 0;
if (this.setOptions)
this.setOptions(options);
else
this.options = options || {};
this.options.paramName = this.options.paramName || this.element.name;
this.options.tokens = this.options.tokens || [];
this.options.frequency = this.options.frequency || 0.4;
this.options.minChars = this.options.minChars || 1;
this.options.onShow = this.options.onShow ||
function(element, update){
if(!update.style.position || update.style.position=='absolute') {
update.style.position = 'absolute';
Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
}
Effect.Appear(update,{duration:0.15});
};
this.options.onHide = this.options.onHide ||
function(element, update){ new Effect.Fade(update,{duration:0.15}) };
if (typeof(this.options.tokens) == 'string')
this.options.tokens = new Array(this.options.tokens);
this.observer = null;
this.element.setAttribute('autocomplete','off');
Element.hide(this.update);
Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
},
show: function() {
if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0) && (Element.getStyle(this.update, 'position')=='absolute')) {
new Insertion.After(this.update,
'<iframe id="' + this.update.id + '_iefix" '+
'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
this.iefix = $(this.update.id+'_iefix');
}
if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
},
fixIEOverlapping: function() {
Position.clone(this.update, this.iefix);
this.iefix.style.zIndex = 1;
this.update.style.zIndex = 2;
Element.show(this.iefix);
},
hide: function() {
this.stopIndicator();
if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
if(this.iefix) Element.hide(this.iefix);
},
startIndicator: function() {
if(this.options.indicator) Element.show(this.options.indicator);
},
stopIndicator: function() {
if(this.options.indicator) Element.hide(this.options.indicator);
},
onKeyPress: function(event) {
if(this.active)
switch(event.keyCode) {
case Event.KEY_TAB:
case Event.KEY_RETURN:
this.selectEntry();
Event.stop(event);
case Event.KEY_ESC:
this.hide();
this.active = false;
Event.stop(event);
return;
case Event.KEY_LEFT:
case Event.KEY_RIGHT:
return;
case Event.KEY_UP:
this.markPrevious();
this.render();
if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
return;
case Event.KEY_DOWN:
this.markNext();
this.render();
if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
return;
}
else
if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN)
return;
this.changed = true;
this.hasFocus = true;
if(this.observer) clearTimeout(this.observer);
this.observer =
setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
},
onHover: function(event) {
var element = Event.findElement(event, 'LI');
if(this.index != element.autocompleteIndex)
{
this.index = element.autocompleteIndex;
this.render();
}
Event.stop(event);
},
onClick: function(event) {
var element = Event.findElement(event, 'LI');
this.index = element.autocompleteIndex;
this.selectEntry();
this.hide();
},
onBlur: function(event) {
// needed to make click events working
setTimeout(this.hide.bind(this), 250);
this.hasFocus = false;
this.active = false;
},
render: function() {
if(this.entryCount > 0) {
for (var i = 0; i < this.entryCount; i++)
this.index==i ?
Element.addClassName(this.getEntry(i),"selected") :
Element.removeClassName(this.getEntry(i),"selected");
if(this.hasFocus) {
this.show();
this.active = true;
}
} else this.hide();
},
markPrevious: function() {
if(this.index > 0) this.index--
else this.index = this.entryCount-1;
},
markNext: function() {
if(this.index < this.entryCount-1) this.index++
else this.index = 0;
},
getEntry: function(index) {
return this.update.firstChild.childNodes[index];
},
getCurrentEntry: function() {
return this.getEntry(this.index);
},
selectEntry: function() {
this.active = false;
this.updateElement(this.getCurrentEntry());
},
updateElement: function(selectedElement) {
if (this.options.updateElement) {
this.options.updateElement(selectedElement);
return;
}
var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
var lastTokenPos = this.findLastToken();
if (lastTokenPos != -1) {
var newValue = this.element.value.substr(0, lastTokenPos + 1);
var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
if (whitespace)
newValue += whitespace[0];
this.element.value = newValue + value;
} else {
this.element.value = value;
}
this.element.focus();
if (this.options.afterUpdateElement)
this.options.afterUpdateElement(this.element, selectedElement);
},
updateChoices: function(choices) {
if(!this.changed && this.hasFocus) {
this.update.innerHTML = choices;
Element.cleanWhitespace(this.update);
Element.cleanWhitespace(this.update.firstChild);
if(this.update.firstChild && this.update.firstChild.childNodes) {
this.entryCount =
this.update.firstChild.childNodes.length;
for (var i = 0; i < this.entryCount; i++) {
var entry = this.getEntry(i);
entry.autocompleteIndex = i;
this.addObservers(entry);
}
} else {
this.entryCount = 0;
}
this.stopIndicator();
this.index = 0;
this.render();
}
},
addObservers: function(element) {
Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
Event.observe(element, "click", this.onClick.bindAsEventListener(this));
},
onObserverEvent: function() {
this.changed = false;
if(this.getToken().length>=this.options.minChars) {
this.startIndicator();
this.getUpdatedChoices();
} else {
this.active = false;
this.hide();
}
},
getToken: function() {
var tokenPos = this.findLastToken();
if (tokenPos != -1)
var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
else
var ret = this.element.value;
return /\n/.test(ret) ? '' : ret;
},
findLastToken: function() {
var lastTokenPos = -1;
for (var i=0; i<this.options.tokens.length; i++) {
var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
if (thisTokenPos > lastTokenPos)
lastTokenPos = thisTokenPos;
}
return lastTokenPos;
}
}
Ajax.Autocompleter = Class.create();
Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
initialize: function(element, update, url, options) {
this.baseInitialize(element, update, options);
this.options.asynchronous = true;
this.options.onComplete = this.onComplete.bind(this);
this.options.defaultParams = this.options.parameters || null;
this.url = url;
},
getUpdatedChoices: function() {
entry = encodeURIComponent(this.options.paramName) + '=' +
encodeURIComponent(this.getToken());
this.options.parameters = this.options.callback ?
this.options.callback(this.element, entry) : entry;
if(this.options.defaultParams)
this.options.parameters += '&' + this.options.defaultParams;
new Ajax.Request(this.url, this.options);
},
onComplete: function(request) {
this.updateChoices(request.responseText);
}
});
// The local array autocompleter. Used when you'd prefer to
// inject an array of autocompletion options into the page, rather
// than sending out Ajax queries, which can be quite slow sometimes.
//
// The constructor takes four parameters. The first two are, as usual,
// the id of the monitored textbox, and id of the autocompletion menu.
// The third is the array you want to autocomplete from, and the fourth
// is the options block.
//
// Extra local autocompletion options:
// - choices - How many autocompletion choices to offer
//
// - partialSearch - If false, the autocompleter will match entered
// text only at the beginning of strings in the
// autocomplete array. Defaults to true, which will
// match text at the beginning of any *word* in the
// strings in the autocomplete array. If you want to
// search anywhere in the string, additionally set
// the option fullSearch to true (default: off).
//
// - fullSsearch - Search anywhere in autocomplete array strings.
//
// - partialChars - How many characters to enter before triggering
// a partial match (unlike minChars, which defines
// how many characters are required to do any match
// at all). Defaults to 2.
//
// - ignoreCase - Whether to ignore case when autocompleting.
// Defaults to true.
//
// It's possible to pass in a custom function as the 'selector'
// option, if you prefer to write your own autocompletion logic.
// In that case, the other options above will not apply unless
// you support them.
Autocompleter.Local = Class.create();
Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
initialize: function(element, update, array, options) {
this.baseInitialize(element, update, options);
this.options.array = array;
},
getUpdatedChoices: function() {
this.updateChoices(this.options.selector(this));
},
setOptions: function(options) {
this.options = Object.extend({
choices: 10,
partialSearch: true,
partialChars: 2,
ignoreCase: true,
fullSearch: false,
selector: function(instance) {
var ret = []; // Beginning matches
var partial = []; // Inside matches
var entry = instance.getToken();
var count = 0;
for (var i = 0; i < instance.options.array.length &&
ret.length < instance.options.choices ; i++) {
var elem = instance.options.array[i];
var foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase()) :
elem.indexOf(entry);
while (foundPos != -1) {
if (foundPos == 0 && elem.length != entry.length) {
ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
elem.substr(entry.length) + "</li>");
break;
} else if (entry.length >= instance.options.partialChars &&
instance.options.partialSearch && foundPos != -1) {
if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
foundPos + entry.length) + "</li>");
break;
}
}
foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
elem.indexOf(entry, foundPos + 1);
}
}
if (partial.length)
ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
return "<ul>" + ret.join('') + "</ul>";
}
}, options || {});
}
});
// AJAX in-place editor
//
// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
Ajax.InPlaceEditor = Class.create();
Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
Ajax.InPlaceEditor.prototype = {
initialize: function(element, url, options) {
this.url = url;
this.element = $(element);
this.options = Object.extend({
okText: "ok",
cancelText: "cancel",
savingText: "Saving...",
clickToEditText: "Click to edit",
okText: "ok",
rows: 1,
onComplete: function(transport, element) {
new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
},
onFailure: function(transport) {
alert("Error communicating with the server: " + transport.responseText.stripTags());
},
callback: function(form) {
return Form.serialize(form);
},
handleLineBreaks: true,
loadingText: 'Loading...',
savingClassName: 'inplaceeditor-saving',
loadingClassName: 'inplaceeditor-loading',
formClassName: 'inplaceeditor-form',
highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
highlightendcolor: "#FFFFFF",
externalControl: null,
ajaxOptions: {}
}, options || {});
if(!this.options.formId && this.element.id) {
this.options.formId = this.element.id + "-inplaceeditor";
if ($(this.options.formId)) {
// there's already a form with that name, don't specify an id
this.options.formId = null;
}
}
if (this.options.externalControl) {
this.options.externalControl = $(this.options.externalControl);
}
this.originalBackground = Element.getStyle(this.element, 'background-color');
if (!this.originalBackground) {
this.originalBackground = "transparent";
}
this.element.title = this.options.clickToEditText;
this.onclickListener = this.enterEditMode.bindAsEventListener(this);
this.mouseoverListener = this.enterHover.bindAsEventListener(this);
this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
Event.observe(this.element, 'click', this.onclickListener);
Event.observe(this.element, 'mouseover', this.mouseoverListener);
Event.observe(this.element, 'mouseout', this.mouseoutListener);
if (this.options.externalControl) {
Event.observe(this.options.externalControl, 'click', this.onclickListener);
Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
}
},
enterEditMode: function() {
if (this.saving) return;
if (this.editing) return;
this.editing = true;
this.onEnterEditMode();
if (this.options.externalControl) {
Element.hide(this.options.externalControl);
}
Element.hide(this.element);
this.createForm();
this.element.parentNode.insertBefore(this.form, this.element);
Field.focus(this.editField);
// stop the event to avoid a page refresh in Safari
if (arguments.length > 1) {
Event.stop(arguments[0]);
}
},
createForm: function() {
this.form = document.createElement("form");
this.form.id = this.options.formId;
Element.addClassName(this.form, this.options.formClassName)
this.form.onsubmit = this.onSubmit.bind(this);
this.createEditField();
if (this.options.textarea) {
var br = document.createElement("br");
this.form.appendChild(br);
}
okButton = document.createElement("input");
okButton.type = "submit";
okButton.value = this.options.okText;
this.form.appendChild(okButton);
cancelLink = document.createElement("a");
cancelLink.href = "#";
cancelLink.appendChild(document.createTextNode(this.options.cancelText));
cancelLink.onclick = this.onclickCancel.bind(this);
this.form.appendChild(cancelLink);
},
hasHTMLLineBreaks: function(string) {
if (!this.options.handleLineBreaks) return false;
return string.match(/<br/i) || string.match(/<p>/i);
},
convertHTMLLineBreaks: function(string) {
return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
},
createEditField: function() {
var text;
if(this.options.loadTextURL) {
text = this.options.loadingText;
} else {
text = this.getText();
}
if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
this.options.textarea = false;
var textField = document.createElement("input");
textField.type = "text";
textField.name = "value";
textField.value = text;
textField.style.backgroundColor = this.options.highlightcolor;
var size = this.options.size || this.options.cols || 0;
if (size != 0) textField.size = size;
this.editField = textField;
} else {
this.options.textarea = true;
var textArea = document.createElement("textarea");
textArea.name = "value";
textArea.value = this.convertHTMLLineBreaks(text);
textArea.rows = this.options.rows;
textArea.cols = this.options.cols || 40;
this.editField = textArea;
}
if(this.options.loadTextURL) {
this.loadExternalText();
}
this.form.appendChild(this.editField);
},
getText: function() {
return this.element.innerHTML;
},
loadExternalText: function() {
Element.addClassName(this.form, this.options.loadingClassName);
this.editField.disabled = true;
new Ajax.Request(
this.options.loadTextURL,
Object.extend({
asynchronous: true,
onComplete: this.onLoadedExternalText.bind(this)
}, this.options.ajaxOptions)
);
},
onLoadedExternalText: function(transport) {
Element.removeClassName(this.form, this.options.loadingClassName);
this.editField.disabled = false;
this.editField.value = transport.responseText.stripTags();
},
onclickCancel: function() {
this.onComplete();
this.leaveEditMode();
return false;
},
onFailure: function(transport) {
this.options.onFailure(transport);
if (this.oldInnerHTML) {
this.element.innerHTML = this.oldInnerHTML;
this.oldInnerHTML = null;
}
return false;
},
onSubmit: function() {
// onLoading resets these so we need to save them away for the Ajax call
var form = this.form;
var value = this.editField.value;
// do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
// which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
// to be displayed indefinitely
this.onLoading();
new Ajax.Updater(
{
success: this.element,
// don't update on failure (this could be an option)
failure: null
},
this.url,
Object.extend({
parameters: this.options.callback(form, value),
onComplete: this.onComplete.bind(this),
onFailure: this.onFailure.bind(this)
}, this.options.ajaxOptions)
);
// stop the event to avoid a page refresh in Safari
if (arguments.length > 1) {
Event.stop(arguments[0]);
}
return false;
},
onLoading: function() {
this.saving = true;
this.removeForm();
this.leaveHover();
this.showSaving();
},
showSaving: function() {
this.oldInnerHTML = this.element.innerHTML;
this.element.innerHTML = this.options.savingText;
Element.addClassName(this.element, this.options.savingClassName);
this.element.style.backgroundColor = this.originalBackground;
Element.show(this.element);
},
removeForm: function() {
if(this.form) {
if (this.form.parentNode) Element.remove(this.form);
this.form = null;
}
},
enterHover: function() {
if (this.saving) return;
this.element.style.backgroundColor = this.options.highlightcolor;
if (this.effect) {
this.effect.cancel();
}
Element.addClassName(this.element, this.options.hoverClassName)
},
leaveHover: function() {
if (this.options.backgroundColor) {
this.element.style.backgroundColor = this.oldBackground;
}
Element.removeClassName(this.element, this.options.hoverClassName)
if (this.saving) return;
this.effect = new Effect.Highlight(this.element, {
startcolor: this.options.highlightcolor,
endcolor: this.options.highlightendcolor,
restorecolor: this.originalBackground
});
},
leaveEditMode: function() {
Element.removeClassName(this.element, this.options.savingClassName);
this.removeForm();
this.leaveHover();
this.element.style.backgroundColor = this.originalBackground;
Element.show(this.element);
if (this.options.externalControl) {
Element.show(this.options.externalControl);
}
this.editing = false;
this.saving = false;
this.oldInnerHTML = null;
this.onLeaveEditMode();
},
onComplete: function(transport) {
this.leaveEditMode();
this.options.onComplete.bind(this)(transport, this.element);
},
onEnterEditMode: function() {},
onLeaveEditMode: function() {},
dispose: function() {
if (this.oldInnerHTML) {
this.element.innerHTML = this.oldInnerHTML;
}
this.leaveEditMode();
Event.stopObserving(this.element, 'click', this.onclickListener);
Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
if (this.options.externalControl) {
Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
}
}
};

516
public/javascripts/dragdrop.js vendored Normal file
View file

@ -0,0 +1,516 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// Element.Class part Copyright (c) 2005 by Rick Olson
//
// See scriptaculous.js for full license.
/*--------------------------------------------------------------------------*/
var Droppables = {
drops: [],
remove: function(element) {
this.drops = this.drops.reject(function(d) { return d.element==element });
},
add: function(element) {
element = $(element);
var options = Object.extend({
greedy: true,
hoverclass: null
}, arguments[1] || {});
// cache containers
if(options.containment) {
options._containers = [];
var containment = options.containment;
if((typeof containment == 'object') &&
(containment.constructor == Array)) {
containment.each( function(c) { options._containers.push($(c)) });
} else {
options._containers.push($(containment));
}
}
Element.makePositioned(element); // fix IE
options.element = element;
this.drops.push(options);
},
isContained: function(element, drop) {
var parentNode = element.parentNode;
return drop._containers.detect(function(c) { return parentNode == c });
},
isAffected: function(pX, pY, element, drop) {
return (
(drop.element!=element) &&
((!drop._containers) ||
this.isContained(element, drop)) &&
((!drop.accept) ||
(Element.Class.has_any(element, drop.accept))) &&
Position.within(drop.element, pX, pY) );
},
deactivate: function(drop) {
if(drop.hoverclass)
Element.Class.remove(drop.element, drop.hoverclass);
this.last_active = null;
},
activate: function(drop) {
if(this.last_active) this.deactivate(this.last_active);
if(drop.hoverclass)
Element.Class.add(drop.element, drop.hoverclass);
this.last_active = drop;
},
show: function(event, element) {
if(!this.drops.length) return;
var pX = Event.pointerX(event);
var pY = Event.pointerY(event);
Position.prepare();
var i = this.drops.length-1; do {
var drop = this.drops[i];
if(this.isAffected(pX, pY, element, drop)) {
if(drop.onHover)
drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
if(drop.greedy) {
this.activate(drop);
return;
}
}
} while (i--);
if(this.last_active) this.deactivate(this.last_active);
},
fire: function(event, element) {
if(!this.last_active) return;
Position.prepare();
if (this.isAffected(Event.pointerX(event), Event.pointerY(event), element, this.last_active))
if (this.last_active.onDrop)
this.last_active.onDrop(element, this.last_active.element, event);
},
reset: function() {
if(this.last_active)
this.deactivate(this.last_active);
}
}
var Draggables = {
observers: [],
addObserver: function(observer) {
this.observers.push(observer);
},
removeObserver: function(element) { // element instead of obsever fixes mem leaks
this.observers = this.observers.reject( function(o) { return o.element==element });
},
notify: function(eventName, draggable) { // 'onStart', 'onEnd'
this.observers.invoke(eventName, draggable);
}
}
/*--------------------------------------------------------------------------*/
var Draggable = Class.create();
Draggable.prototype = {
initialize: function(element) {
var options = Object.extend({
handle: false,
starteffect: function(element) {
new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7});
},
reverteffect: function(element, top_offset, left_offset) {
var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
new Effect.MoveBy(element, -top_offset, -left_offset, {duration:dur});
},
endeffect: function(element) {
new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0});
},
zindex: 1000,
revert: false
}, arguments[1] || {});
this.element = $(element);
if(options.handle && (typeof options.handle == 'string'))
this.handle = Element.Class.childrenWith(this.element, options.handle)[0];
if(!this.handle) this.handle = $(options.handle);
if(!this.handle) this.handle = this.element;
Element.makePositioned(this.element); // fix IE
this.offsetX = 0;
this.offsetY = 0;
this.originalLeft = this.currentLeft();
this.originalTop = this.currentTop();
this.originalX = this.element.offsetLeft;
this.originalY = this.element.offsetTop;
this.options = options;
this.active = false;
this.dragging = false;
this.eventMouseDown = this.startDrag.bindAsEventListener(this);
this.eventMouseUp = this.endDrag.bindAsEventListener(this);
this.eventMouseMove = this.update.bindAsEventListener(this);
this.eventKeypress = this.keyPress.bindAsEventListener(this);
this.registerEvents();
},
destroy: function() {
Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
this.unregisterEvents();
},
registerEvents: function() {
Event.observe(document, "mouseup", this.eventMouseUp);
Event.observe(document, "mousemove", this.eventMouseMove);
Event.observe(document, "keypress", this.eventKeypress);
Event.observe(this.handle, "mousedown", this.eventMouseDown);
},
unregisterEvents: function() {
//if(!this.active) return;
//Event.stopObserving(document, "mouseup", this.eventMouseUp);
//Event.stopObserving(document, "mousemove", this.eventMouseMove);
//Event.stopObserving(document, "keypress", this.eventKeypress);
},
currentLeft: function() {
return parseInt(this.element.style.left || '0');
},
currentTop: function() {
return parseInt(this.element.style.top || '0')
},
startDrag: function(event) {
if(Event.isLeftClick(event)) {
// abort on form elements, fixes a Firefox issue
var src = Event.element(event);
if(src.tagName && (
src.tagName=='INPUT' ||
src.tagName=='SELECT' ||
src.tagName=='BUTTON' ||
src.tagName=='TEXTAREA')) return;
// this.registerEvents();
this.active = true;
var pointer = [Event.pointerX(event), Event.pointerY(event)];
var offsets = Position.cumulativeOffset(this.element);
this.offsetX = (pointer[0] - offsets[0]);
this.offsetY = (pointer[1] - offsets[1]);
Event.stop(event);
}
},
finishDrag: function(event, success) {
// this.unregisterEvents();
this.active = false;
this.dragging = false;
if(this.options.ghosting) {
Position.relativize(this.element);
Element.remove(this._clone);
this._clone = null;
}
if(success) Droppables.fire(event, this.element);
Draggables.notify('onEnd', this);
var revert = this.options.revert;
if(revert && typeof revert == 'function') revert = revert(this.element);
if(revert && this.options.reverteffect) {
this.options.reverteffect(this.element,
this.currentTop()-this.originalTop,
this.currentLeft()-this.originalLeft);
} else {
this.originalLeft = this.currentLeft();
this.originalTop = this.currentTop();
}
if(this.options.zindex)
this.element.style.zIndex = this.originalZ;
if(this.options.endeffect)
this.options.endeffect(this.element);
Droppables.reset();
},
keyPress: function(event) {
if(this.active) {
if(event.keyCode==Event.KEY_ESC) {
this.finishDrag(event, false);
Event.stop(event);
}
}
},
endDrag: function(event) {
if(this.active && this.dragging) {
this.finishDrag(event, true);
Event.stop(event);
}
this.active = false;
this.dragging = false;
},
draw: function(event) {
var pointer = [Event.pointerX(event), Event.pointerY(event)];
var offsets = Position.cumulativeOffset(this.element);
offsets[0] -= this.currentLeft();
offsets[1] -= this.currentTop();
var style = this.element.style;
if((!this.options.constraint) || (this.options.constraint=='horizontal'))
style.left = (pointer[0] - offsets[0] - this.offsetX) + "px";
if((!this.options.constraint) || (this.options.constraint=='vertical'))
style.top = (pointer[1] - offsets[1] - this.offsetY) + "px";
if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
},
update: function(event) {
if(this.active) {
if(!this.dragging) {
var style = this.element.style;
this.dragging = true;
if(Element.getStyle(this.element,'position')=='')
style.position = "relative";
if(this.options.zindex) {
this.options.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
style.zIndex = this.options.zindex;
}
if(this.options.ghosting) {
this._clone = this.element.cloneNode(true);
Position.absolutize(this.element);
this.element.parentNode.insertBefore(this._clone, this.element);
}
Draggables.notify('onStart', this);
if(this.options.starteffect) this.options.starteffect(this.element);
}
Droppables.show(event, this.element);
this.draw(event);
if(this.options.change) this.options.change(this);
// fix AppleWebKit rendering
if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
Event.stop(event);
}
}
}
/*--------------------------------------------------------------------------*/
var SortableObserver = Class.create();
SortableObserver.prototype = {
initialize: function(element, observer) {
this.element = $(element);
this.observer = observer;
this.lastValue = Sortable.serialize(this.element);
},
onStart: function() {
this.lastValue = Sortable.serialize(this.element);
},
onEnd: function() {
Sortable.unmark();
if(this.lastValue != Sortable.serialize(this.element))
this.observer(this.element)
}
}
var Sortable = {
sortables: new Array(),
options: function(element){
element = $(element);
return this.sortables.detect(function(s) { return s.element == element });
},
destroy: function(element){
element = $(element);
this.sortables.findAll(function(s) { return s.element == element }).each(function(s){
Draggables.removeObserver(s.element);
s.droppables.each(function(d){ Droppables.remove(d) });
s.draggables.invoke('destroy');
});
this.sortables = this.sortables.reject(function(s) { return s.element == element });
},
create: function(element) {
element = $(element);
var options = Object.extend({
element: element,
tag: 'li', // assumes li children, override with tag: 'tagname'
dropOnEmpty: false,
tree: false, // fixme: unimplemented
overlap: 'vertical', // one of 'vertical', 'horizontal'
constraint: 'vertical', // one of 'vertical', 'horizontal', false
containment: element, // also takes array of elements (or id's); or false
handle: false, // or a CSS class
only: false,
hoverclass: null,
ghosting: false,
format: null,
onChange: function() {},
onUpdate: function() {}
}, arguments[1] || {});
// clear any old sortable with same element
this.destroy(element);
// build options for the draggables
var options_for_draggable = {
revert: true,
ghosting: options.ghosting,
constraint: options.constraint,
handle: options.handle };
if(options.starteffect)
options_for_draggable.starteffect = options.starteffect;
if(options.reverteffect)
options_for_draggable.reverteffect = options.reverteffect;
else
if(options.ghosting) options_for_draggable.reverteffect = function(element) {
element.style.top = 0;
element.style.left = 0;
};
if(options.endeffect)
options_for_draggable.endeffect = options.endeffect;
if(options.zindex)
options_for_draggable.zindex = options.zindex;
// build options for the droppables
var options_for_droppable = {
overlap: options.overlap,
containment: options.containment,
hoverclass: options.hoverclass,
onHover: Sortable.onHover,
greedy: !options.dropOnEmpty
}
// fix for gecko engine
Element.cleanWhitespace(element);
options.draggables = [];
options.droppables = [];
// make it so
// drop on empty handling
if(options.dropOnEmpty) {
Droppables.add(element,
{containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false});
options.droppables.push(element);
}
(this.findElements(element, options) || []).each( function(e) {
// handles are per-draggable
var handle = options.handle ?
Element.Class.childrenWith(e, options.handle)[0] : e;
options.draggables.push(
new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
Droppables.add(e, options_for_droppable);
options.droppables.push(e);
});
// keep reference
this.sortables.push(options);
// for onupdate
Draggables.addObserver(new SortableObserver(element, options.onUpdate));
},
// return all suitable-for-sortable elements in a guaranteed order
findElements: function(element, options) {
if(!element.hasChildNodes()) return null;
var elements = [];
$A(element.childNodes).each( function(e) {
if(e.tagName && e.tagName==options.tag.toUpperCase() &&
(!options.only || (Element.Class.has(e, options.only))))
elements.push(e);
if(options.tree) {
var grandchildren = this.findElements(e, options);
if(grandchildren) elements.push(grandchildren);
}
});
return (elements.length>0 ? elements.flatten() : null);
},
onHover: function(element, dropon, overlap) {
if(overlap>0.5) {
Sortable.mark(dropon, 'before');
if(dropon.previousSibling != element) {
var oldParentNode = element.parentNode;
element.style.visibility = "hidden"; // fix gecko rendering
dropon.parentNode.insertBefore(element, dropon);
if(dropon.parentNode!=oldParentNode)
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon.parentNode).onChange(element);
}
} else {
Sortable.mark(dropon, 'after');
var nextElement = dropon.nextSibling || null;
if(nextElement != element) {
var oldParentNode = element.parentNode;
element.style.visibility = "hidden"; // fix gecko rendering
dropon.parentNode.insertBefore(element, nextElement);
if(dropon.parentNode!=oldParentNode)
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon.parentNode).onChange(element);
}
}
},
onEmptyHover: function(element, dropon) {
if(element.parentNode!=dropon) {
dropon.appendChild(element);
}
},
unmark: function() {
if(Sortable._marker) Element.hide(Sortable._marker);
},
mark: function(dropon, position) {
// mark on ghosting only
var sortable = Sortable.options(dropon.parentNode);
if(sortable && !sortable.ghosting) return;
if(!Sortable._marker) {
Sortable._marker = $('dropmarker') || document.createElement('DIV');
Element.hide(Sortable._marker);
Element.Class.add(Sortable._marker, 'dropmarker');
Sortable._marker.style.position = 'absolute';
document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
}
var offsets = Position.cumulativeOffset(dropon);
Sortable._marker.style.top = offsets[1] + 'px';
if(position=='after') Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
Sortable._marker.style.left = offsets[0] + 'px';
Element.show(Sortable._marker);
},
serialize: function(element) {
element = $(element);
var sortableOptions = this.options(element);
var options = Object.extend({
tag: sortableOptions.tag,
only: sortableOptions.only,
name: element.id,
format: sortableOptions.format || /^[^_]*_(.*)$/
}, arguments[1] || {});
return $(this.findElements(element, options) || []).collect( function(item) {
return (encodeURIComponent(options.name) + "[]=" +
encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : ''));
}).join("&");
}
}

1101
public/javascripts/effects.js vendored Normal file

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more