sugoi-mail 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +35 -0
- data/Rakefile +10 -0
- data/app/apis/mailservice_api.rb +178 -0
- data/app/controllers/.sugoi_admin_controller.rb.swp +0 -0
- data/app/controllers/account_controller.rb +39 -0
- data/app/controllers/address_controller.rb +49 -0
- data/app/controllers/application.rb +4 -0
- data/app/controllers/commandline_controller.rb +15 -0
- data/app/controllers/domain_controller.rb +37 -0
- data/app/controllers/mailinglist_controller.rb +51 -0
- data/app/controllers/mailservice_controller.rb +497 -0
- data/app/controllers/sugoi_admin_controller.rb +93 -0
- data/app/helpers/account_helper.rb +2 -0
- data/app/helpers/address_helper.rb +2 -0
- data/app/helpers/application_helper.rb +3 -0
- data/app/helpers/domain_helper.rb +2 -0
- data/app/helpers/mailservice_helper.rb +2 -0
- data/app/helpers/sugoi_admin_helper.rb +2 -0
- data/app/models/address.rb +110 -0
- data/app/models/admin_message.rb +45 -0
- data/app/models/confirmationcode.rb +59 -0
- data/app/models/domain.rb +76 -0
- data/app/models/mailinglist.rb +336 -0
- data/app/models/mailinglist_class.rb +39 -0
- data/app/models/message.rb +293 -0
- data/app/models/proxy_link.rb +25 -0
- data/app/models/user.rb +150 -0
- data/app/views/account/login.rhtml +22 -0
- data/app/views/account/logout.rhtml +10 -0
- data/app/views/account/signup.rhtml +17 -0
- data/app/views/account/welcome.rhtml +13 -0
- data/app/views/address/_form.rhtml +7 -0
- data/app/views/address/edit.rhtml +10 -0
- data/app/views/address/list.rhtml +27 -0
- data/app/views/address/new.rhtml +8 -0
- data/app/views/address/show.rhtml +8 -0
- data/app/views/domain/login.rhtml +22 -0
- data/app/views/domain/logout.rhtml +10 -0
- data/app/views/domain/signup.rhtml +17 -0
- data/app/views/domain/welcome.rhtml +13 -0
- data/app/views/layouts/address.rhtml +13 -0
- data/app/views/layouts/scaffold.rhtml +13 -0
- data/app/views/mailinglist/_form.rhtml +28 -0
- data/app/views/mailinglist/edit.rhtml +10 -0
- data/app/views/mailinglist/list.rhtml +27 -0
- data/app/views/mailinglist/new.rhtml +8 -0
- data/app/views/mailinglist/show.rhtml +16 -0
- data/app/views/sugoi_admin/create_domain.rhtml +1 -0
- data/app/views/sugoi_admin/list_addresses.rhtml +1 -0
- data/app/views/sugoi_admin/list_domains.rhtml +2 -0
- data/app/views/sugoi_admin/list_mailinglists.rhtml +1 -0
- data/bin/mailc +32 -0
- data/bin/maild +133 -0
- data/bin/sugoi-admin +21 -0
- data/bin/sugoi-mail +20 -0
- data/config/boot.rb +44 -0
- data/config/environment.rb +54 -0
- data/config/environments/bench.rb +21 -0
- data/config/environments/coverage.rb +21 -0
- data/config/environments/development.rb +21 -0
- data/config/environments/production.rb +18 -0
- data/config/environments/test.rb +19 -0
- data/config/lighttpd.conf +46 -0
- data/config/routes.rb +29 -0
- data/db/migrate/001_mailproxy.rb +7 -0
- data/db/migrate/002_create_users.rb +13 -0
- data/db/migrate/003_create_mailinglists.rb +13 -0
- data/db/migrate/004_create_addresses.rb +12 -0
- data/db/migrate/005_create_addresses_mailinglists.rb +13 -0
- data/db/migrate/006_alter_mailinglists.rb +9 -0
- data/db/migrate/007_create_messages.rb +25 -0
- data/db/migrate/008_add_mailinglistid_to_users.rb +14 -0
- data/db/migrate/009_add_domainadmin_to_users.rb +9 -0
- data/db/migrate/010_add_domain_to_users.rb +16 -0
- data/db/migrate/011_add_active_to_addresses.rb +14 -0
- data/db/migrate/012_create_confirmationcodes.rb +14 -0
- data/db/migrate/013_add_description_to_mailinglists.rb +9 -0
- data/db/migrate/014_create_admin_messages.rb +69 -0
- data/db/migrate/015_add_messages_to_mailinglists.rb +26 -0
- data/db/migrate/016_add_mailinglist_admin_to_users.rb +9 -0
- data/db/migrate/017_add_mailinglist_types.rb +94 -0
- data/db/migrate/018_add_bounciness_to_addresses.rb +20 -0
- data/db/migrate/019_add_archived_to_mailinglist_classes.rb +25 -0
- data/db/migrate/020_add_envelope_data_to_messages.rb +11 -0
- data/db/migrate/021_add_addresses_users.rb +14 -0
- data/db/migrate/022_add_virtual_to_users.rb +14 -0
- data/db/migrate/023_drop_openposting_from_mailinglists.rb +9 -0
- data/db/migrate/024_add_proxy_links.rb +21 -0
- data/db/migrate/025_add_proxify_to_mailinglist_classes.rb +25 -0
- data/db/schema.mysql.sql +104 -0
- data/db/schema.postgresql.sql +104 -0
- data/db/schema.rb +85 -0
- data/db/schema.sqlite.sql +104 -0
- data/db/schema.sqlserver.sql +113 -0
- data/db/schema_version +1 -0
- data/doc/README_FOR_APP +179 -0
- data/doc/mailinglist_classes description.txt +28 -0
- data/installer/rails_installer_defaults.yml +5 -0
- data/lib/daemonize.rb +56 -0
- data/lib/domain_system.rb +87 -0
- data/lib/gurgitate-rules.rb +69 -0
- data/lib/limitedfork.rb +66 -0
- data/lib/login_system.rb +87 -0
- data/public/.htaccess +40 -0
- data/public/404.html +8 -0
- data/public/500.html +8 -0
- data/public/dispatch.cgi +10 -0
- data/public/dispatch.fcgi +24 -0
- data/public/dispatch.rb +10 -0
- data/public/favicon.ico +0 -0
- data/public/images/rails.png +0 -0
- data/public/javascripts/application.js +2 -0
- data/public/javascripts/controls.js +815 -0
- data/public/javascripts/dragdrop.js +913 -0
- data/public/javascripts/effects.js +958 -0
- data/public/javascripts/prototype.js +2006 -0
- data/public/robots.txt +1 -0
- data/public/stylesheets/scaffold.css +74 -0
- data/public/stylesheets/trestle.css +74 -0
- data/script/about +3 -0
- data/script/breakpointer +3 -0
- data/script/console +3 -0
- data/script/destroy +3 -0
- data/script/fakedeliver +19 -0
- data/script/generate +3 -0
- data/script/performance/benchmarker +3 -0
- data/script/performance/profiler +3 -0
- data/script/plugin +3 -0
- data/script/process/reaper +3 -0
- data/script/process/spawner +3 -0
- data/script/runner +3 -0
- data/script/server +3 -0
- data/sugoi-mail.gemspec +36 -0
- data/test/fixtures/addresses.yml +70 -0
- data/test/fixtures/addresses_mailinglists.yml +20 -0
- data/test/fixtures/admin_messages.yml +65 -0
- data/test/fixtures/confirmationcodes.yml +13 -0
- data/test/fixtures/domains.yml +9 -0
- data/test/fixtures/mailinglist_classes.yml +45 -0
- data/test/fixtures/mailinglists.yml +80 -0
- data/test/fixtures/messages.yml +154 -0
- data/test/fixtures/proxy_links.yml +5 -0
- data/test/fixtures/users.yml +50 -0
- data/test/functional/domain_controller_test.rb +74 -0
- data/test/functional/mailservice_controller_test.rb +546 -0
- data/test/integration/test_soap.rb +413 -0
- data/test/integration/test_xmlrpc.rb +198 -0
- data/test/mocks/test/net-smtp-stub.rb +24 -0
- data/test/test_helper.rb +28 -0
- data/test/unit/address_test.rb +44 -0
- data/test/unit/admin_message_test.rb +41 -0
- data/test/unit/confirmationcode_test.rb +64 -0
- data/test/unit/domain_test.rb +128 -0
- data/test/unit/mailinglist_class_test.rb +82 -0
- data/test/unit/mailinglist_test.rb +145 -0
- data/test/unit/message_test.rb +151 -0
- data/test/unit/user_test.rb +126 -0
- data/test/units.rb +4 -0
- data/vendor/plugins/active_command/init.rb +1 -0
- data/vendor/plugins/active_command/lib/active_command/request.rb +137 -0
- data/vendor/plugins/active_command/lib/active_command/response.rb +132 -0
- data/vendor/plugins/active_command/lib/active_command.rb +2 -0
- data/vendor/plugins/ar_fixtures/CHANGELOG +10 -0
- data/vendor/plugins/ar_fixtures/MIT-LICENSE +20 -0
- data/vendor/plugins/ar_fixtures/README +19 -0
- data/vendor/plugins/ar_fixtures/Rakefile +54 -0
- data/vendor/plugins/ar_fixtures/about.yml +7 -0
- data/vendor/plugins/ar_fixtures/init.rb +1 -0
- data/vendor/plugins/ar_fixtures/lib/ar_fixtures.rb +102 -0
- data/vendor/plugins/ar_fixtures/tasks/ar_fixtures.rake +39 -0
- data/vendor/plugins/ar_fixtures/test/ar_fixtures_test.rb +53 -0
- data/vendor/plugins/ar_fixtures/test/database.yml +18 -0
- data/vendor/plugins/ar_fixtures/test/fixtures/beer.rb +5 -0
- data/vendor/plugins/ar_fixtures/test/fixtures/beers.yml +9 -0
- data/vendor/plugins/ar_fixtures/test/fixtures/beers_drunkards.yml +8 -0
- data/vendor/plugins/ar_fixtures/test/fixtures/drunkard.rb +6 -0
- data/vendor/plugins/ar_fixtures/test/fixtures/drunkards.yml +8 -0
- data/vendor/plugins/ar_fixtures/test/fixtures/glass.rb +2 -0
- data/vendor/plugins/ar_fixtures/test/fixtures/glasses.yml +9 -0
- data/vendor/plugins/ar_fixtures/test/schema.rb +21 -0
- data/vendor/plugins/ar_fixtures/test/test_helper.rb +37 -0
- metadata +320 -0
@@ -0,0 +1,110 @@
|
|
1
|
+
# The addresses table is where all the email addresses are stored, and
|
2
|
+
# Address is the interface to that.
|
3
|
+
#
|
4
|
+
# Address has the following data members:
|
5
|
+
# +id+:: The row ID in the database.
|
6
|
+
# +address+:: The actual email address
|
7
|
+
# +active+:: Whether the address is active or not. This should
|
8
|
+
# be reset to false upon a bounce, but currently
|
9
|
+
# isn't because there isn't a standard format for
|
10
|
+
# bounce messages and I haven't yet written the
|
11
|
+
# code to figure out which address I attempted to
|
12
|
+
# send mail to failed.
|
13
|
+
# +delivery_attempts+:: The number of delivery attempts. This is also
|
14
|
+
# related to bouncing, and is thus a stub.
|
15
|
+
# +bounces+:: The number of times email sent to this address
|
16
|
+
# has bounced. Like the other two bounce-related
|
17
|
+
# members, this is merely a stub.
|
18
|
+
#
|
19
|
+
# Address is also joined with other classes:
|
20
|
+
# Mailinglist:: The join between Mailinglist and Address reflects
|
21
|
+
# addresses are subscribed to a mailing list.
|
22
|
+
# User:: The join between Mailinglist and User reflects
|
23
|
+
# addresses that the User can send email to via the
|
24
|
+
# proxy in order to trigger email-address rewriting.
|
25
|
+
# Message:: Messages that were received from this Address.
|
26
|
+
|
27
|
+
class Address < ActiveRecord::Base
|
28
|
+
# The mailing lists that the address is a subscriber to
|
29
|
+
has_and_belongs_to_many :mailinglists
|
30
|
+
has_many :proxy_links
|
31
|
+
|
32
|
+
# The messages that were sent to this address
|
33
|
+
has_many :messages
|
34
|
+
|
35
|
+
# Parses the address string in +addrstr+ for an actual email address.
|
36
|
+
# This handles addresses of the forms:
|
37
|
+
#
|
38
|
+
# Dave Brown <dagbrown@mailman.jp>
|
39
|
+
# "Dave A. Brown" <dagbrown@mailman.jp>
|
40
|
+
# dagbrown@mailman.jp (Dave Brown)
|
41
|
+
# dagbrown@mailman.jp
|
42
|
+
#
|
43
|
+
# In other words, things that are likely to be in the From: and To: headers
|
44
|
+
# in an average email message.
|
45
|
+
#
|
46
|
+
# addrstr::
|
47
|
+
# The email address string.
|
48
|
+
def self.parse(addrstr) # not really OO, this
|
49
|
+
matches = /\s*([^@]+\@\S+) \((.*)\)/.match(addrstr)
|
50
|
+
if matches
|
51
|
+
return matches[1], matches[2]
|
52
|
+
end
|
53
|
+
|
54
|
+
matches = /(\".*\") \<([^@]+\@\S+)\>/.match(addrstr)
|
55
|
+
if matches
|
56
|
+
return matches[2], matches[1]
|
57
|
+
end
|
58
|
+
|
59
|
+
matches = /((?:\w|\s)*) \<([^@]+\@\S+)\>/.match(addrstr)
|
60
|
+
if matches
|
61
|
+
return matches[2], matches[1]
|
62
|
+
end
|
63
|
+
|
64
|
+
matches = /(\S+@\S+)/.match(addrstr)
|
65
|
+
if matches
|
66
|
+
return matches[1],nil
|
67
|
+
end
|
68
|
+
|
69
|
+
return addrstr, nil # and this is being probably needlessly optimistic
|
70
|
+
end
|
71
|
+
|
72
|
+
# If the address in +addrstr+ has a proxy address, returns the proxy
|
73
|
+
# address.
|
74
|
+
#
|
75
|
+
# addrstr::
|
76
|
+
# The address string to search for a proxy address for
|
77
|
+
def self.proxyaddress(addrstr)
|
78
|
+
a = Address.find_or_create_by_address(addrstr) and a.proxyaddress
|
79
|
+
end
|
80
|
+
|
81
|
+
# If the current address has a proxy address, return that.
|
82
|
+
def proxyaddress
|
83
|
+
user && user.address
|
84
|
+
end
|
85
|
+
|
86
|
+
def outside?
|
87
|
+
not user
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns an anonymous proxy address for this user and a mailing list
|
91
|
+
def proxified ml
|
92
|
+
ProxyLink.find_or_create_by_address_id_and_mailinglist_id(
|
93
|
+
id, ml.id
|
94
|
+
).proxy_address
|
95
|
+
end
|
96
|
+
|
97
|
+
# If the address has a proxy address, then return the user that
|
98
|
+
# the proxy address belongs to.
|
99
|
+
def user
|
100
|
+
ml = mailinglists.find_all do |ml|
|
101
|
+
ml.id == ml.user.mailinglist_id && !ml.user.virtual?
|
102
|
+
end[0]
|
103
|
+
if ml then ml.user end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Return a string representation of the address.
|
107
|
+
def to_s
|
108
|
+
address
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "net/smtp"
|
2
|
+
|
3
|
+
# Admin messages are the messages that are sent out when people are joining
|
4
|
+
# or leaving a mailing list.
|
5
|
+
class AdminMessage < ActiveRecord::Base
|
6
|
+
# The regular expression defining what templates look like. The
|
7
|
+
# templates are the things like {address} that are replaced with
|
8
|
+
# an actual address.
|
9
|
+
TemplateRegex = /\{(\w+)\}/
|
10
|
+
|
11
|
+
# Renders the current administration message, filling in the template with
|
12
|
+
# the values with the values in +template_values+. For example, if the
|
13
|
+
# message is:
|
14
|
+
#
|
15
|
+
# Thank you for subscribing to the "{mldesc}" mailing list.
|
16
|
+
#
|
17
|
+
# You might call #render as such:
|
18
|
+
#
|
19
|
+
# adminmsg.render :mldesc => "quick example"
|
20
|
+
#
|
21
|
+
# This would return:
|
22
|
+
#
|
23
|
+
# Thank you for subscribing to the "quick example" mailing list.
|
24
|
+
#
|
25
|
+
# template_values::
|
26
|
+
# A Hash containing as keys, template keys, and as values, the
|
27
|
+
# values to replace the template keys with.
|
28
|
+
def render template_values
|
29
|
+
template_values.each_key do |k|
|
30
|
+
template_values[k.to_s]=template_values[k]
|
31
|
+
end
|
32
|
+
self.message.gsub(TemplateRegex) do |text|
|
33
|
+
template_values[ text.match(TemplateRegex)[1] ]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Sends out an admin message with with the +from+ address in the
|
38
|
+
# envelope "From" string, the +to+ address in the envelope "To" string,
|
39
|
+
# and with with +template_values+ expanded as in +render+.1
|
40
|
+
def send_mail from, to, template_values
|
41
|
+
Net::SMTP.start("localhost", 25) do |smtp|
|
42
|
+
smtp.send_message render(template_values), from, to
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "net/smtp"
|
2
|
+
|
3
|
+
# The Confirmation code is a sort of join table that links mailing lists
|
4
|
+
# with addresses, and also stores confirmation status.
|
5
|
+
class Confirmationcode < ActiveRecord::Base
|
6
|
+
belongs_to :mailinglist
|
7
|
+
belongs_to :address
|
8
|
+
|
9
|
+
# Creates a new confirmation code, and then freezes it so that it
|
10
|
+
# can't be modified.
|
11
|
+
def initialize
|
12
|
+
super
|
13
|
+
@attributes["code"]=("%16s" % rand(36**16).to_s(36)).gsub(/ /,"0")
|
14
|
+
@attributes["code"].freeze
|
15
|
+
end
|
16
|
+
|
17
|
+
validates_presence_of :mailinglist
|
18
|
+
validates_presence_of :address
|
19
|
+
|
20
|
+
def code= _
|
21
|
+
raise TypeError, "can't modify code string"
|
22
|
+
end
|
23
|
+
|
24
|
+
# Given a +mailinglist_id+ and an +address_id+, returns whether the
|
25
|
+
# address has been confirmed for that mailing list or not.
|
26
|
+
def self.confirmed? mailinglist_id, address_id
|
27
|
+
confirmation_record = find( :first, :conditions =>
|
28
|
+
[ 'mailinglist_id = ? and address_id = ?',
|
29
|
+
mailinglist_id, address_id ] )
|
30
|
+
if confirmation_record then
|
31
|
+
confirmation_record.confirmed
|
32
|
+
else
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Given a +mailinglist+, an +address+ and a confirmation code in
|
38
|
+
# +code+, it confirms the address in that mailing list if the
|
39
|
+
# confirmation code is correct.
|
40
|
+
#
|
41
|
+
# If the confirmation code is incorrect, returns false.
|
42
|
+
def self.confirm mailinglist, address, code
|
43
|
+
confirmation_record =
|
44
|
+
find(:first, :conditions => [
|
45
|
+
'mailinglist_id = ? and address_id = ? and code = ?',
|
46
|
+
mailinglist.id,
|
47
|
+
address.id,
|
48
|
+
code
|
49
|
+
])
|
50
|
+
|
51
|
+
if confirmation_record then
|
52
|
+
confirmation_record.confirmed = true
|
53
|
+
if confirmation_record.save then
|
54
|
+
true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
|
3
|
+
# = Domain
|
4
|
+
#
|
5
|
+
# This is actually nothing more than a User class generated by the
|
6
|
+
# standard Rails login generator.
|
7
|
+
#
|
8
|
+
# Its data structure is very basic, as a result:
|
9
|
+
# name:: The domain's name.
|
10
|
+
# password:: A SHA1 hash of the domain's password.
|
11
|
+
#
|
12
|
+
# Its use as a login service is mainly for the purposes of the
|
13
|
+
# Mailservice web service--the middle layer (the SOAP client) logs into
|
14
|
+
# the domain, and then the users can log into _that_.
|
15
|
+
class Domain < ActiveRecord::Base
|
16
|
+
|
17
|
+
#----------------------------------------
|
18
|
+
# First of all, email address stuff
|
19
|
+
#----------------------------------------
|
20
|
+
has_many :users
|
21
|
+
|
22
|
+
# Please change the salt to something else,
|
23
|
+
# Every application should use a different one
|
24
|
+
@@salt = 'thuchpog10.?guf'
|
25
|
+
cattr_accessor :salt
|
26
|
+
|
27
|
+
# Authenticate a user.
|
28
|
+
#
|
29
|
+
# Example:
|
30
|
+
# @user = User.authenticate('bob', 'bobpass')
|
31
|
+
#
|
32
|
+
def self.authenticate(name, pass)
|
33
|
+
find_first(["name = ? AND password = ?", name, sha1(pass)])
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
# Apply SHA1 encryption to the supplied password.
|
40
|
+
# We will additionally surround the password with a salt
|
41
|
+
# for additional security.
|
42
|
+
def self.sha1(pass)
|
43
|
+
Digest::SHA1.hexdigest("#{salt}--#{pass}--")
|
44
|
+
end
|
45
|
+
|
46
|
+
before_create :crypt_password
|
47
|
+
|
48
|
+
# Before saving the record to database we will crypt the password
|
49
|
+
# using SHA1.
|
50
|
+
# We never store the actual password in the DB.
|
51
|
+
def crypt_password
|
52
|
+
write_attribute "password", self.class.sha1(password)
|
53
|
+
end
|
54
|
+
|
55
|
+
before_update :crypt_unless_empty
|
56
|
+
|
57
|
+
# If the record is updated we will check if the password is empty.
|
58
|
+
# If its empty we assume that the user didn't want to change his
|
59
|
+
# password and just reset it to the old value.
|
60
|
+
def crypt_unless_empty
|
61
|
+
if password.empty?
|
62
|
+
user = self.class.find(self.id)
|
63
|
+
self.password = user.password
|
64
|
+
else
|
65
|
+
write_attribute "password", self.class.sha1(password)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
validates_uniqueness_of :name, :on => :create
|
70
|
+
|
71
|
+
validates_confirmation_of :password
|
72
|
+
validates_length_of :name, :within => 3..40
|
73
|
+
validates_length_of :password, :within => 5..40
|
74
|
+
validates_presence_of :name, :password, :password_confirmation,
|
75
|
+
:on => :create
|
76
|
+
end
|
@@ -0,0 +1,336 @@
|
|
1
|
+
# Mailing lists are what make Sugoi-Mail go. Which makes sense, I suppose,
|
2
|
+
# since Sugoi-Mail is a mailing list manager.
|
3
|
+
#
|
4
|
+
# A Mailing List has the following structure:
|
5
|
+
#
|
6
|
+
# (from the schema)
|
7
|
+
#
|
8
|
+
# name: The name of the mailing list. This will
|
9
|
+
# become the local part of the list's email
|
10
|
+
# address.
|
11
|
+
# description:: The description of the mailing list. This
|
12
|
+
# go into the "real name" portion of the mailing
|
13
|
+
# list's email address listed in the "From:"
|
14
|
+
# header in messages that are sent out to
|
15
|
+
# subscribers.
|
16
|
+
# user:: The Sugoi-Mail user who owns and manages the
|
17
|
+
# mailing list--this is a User object. The domain
|
18
|
+
# of the mailing list is the domain that the user
|
19
|
+
# is a member of.
|
20
|
+
# mailinglist_class:: The kind of mailing list that this is. This is
|
21
|
+
# a MailinglistClass object.
|
22
|
+
# welcome_admin_message:: The message that is sent out to a new member
|
23
|
+
# when a subscription is received. This is an
|
24
|
+
# AdminMessage object.
|
25
|
+
# confirmed_admin_message:: The message that is sent out to a new member
|
26
|
+
# when the subscription confirmation message
|
27
|
+
# is confirmed. This is also an AdminMessage
|
28
|
+
# object.
|
29
|
+
# sayonara_admin_message_id:: The final AdminMessage object (as of the moment
|
30
|
+
# anyway--couriermlm mailing lists have many more
|
31
|
+
# administrative messages, and I suspect I may
|
32
|
+
# have to implement all those at some point).
|
33
|
+
# This is the message that is sent out to a
|
34
|
+
# subscriber when she unsubscribes from the
|
35
|
+
# mailing list.
|
36
|
+
|
37
|
+
class Mailinglist < ActiveRecord::Base
|
38
|
+
belongs_to :user
|
39
|
+
has_and_belongs_to_many :addresses
|
40
|
+
has_many :messages
|
41
|
+
has_many :proxy_links
|
42
|
+
validates_each :name, :on => :create do |record, attr, value|
|
43
|
+
if record.user
|
44
|
+
if Mailinglist.find_by_sql(
|
45
|
+
['select * from mailinglists m, users u
|
46
|
+
where u.id = m.user_id
|
47
|
+
and u.domain_id = ?
|
48
|
+
and m.name = ?',
|
49
|
+
record.user.domain_id,
|
50
|
+
value ]).length > 0 then
|
51
|
+
record.errors.add("name",
|
52
|
+
"Address already exists: #{record.address}")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
validates_associated :user, :on => :create
|
58
|
+
|
59
|
+
validates_presence_of :user
|
60
|
+
belongs_to :mailinglist_class
|
61
|
+
|
62
|
+
# Since rails's has_one interface apparently DOESN'T WORK, I
|
63
|
+
# have to do all these by hand.
|
64
|
+
|
65
|
+
%w{welcome confirmed sayonara}.each do |msgtype|
|
66
|
+
eval <<-EOT
|
67
|
+
def #{msgtype}_admin_message
|
68
|
+
AdminMessage.find #{msgtype}_admin_message_id
|
69
|
+
end
|
70
|
+
|
71
|
+
def #{msgtype}_admin_message=(msg)
|
72
|
+
self.#{msgtype}_admin_message_id = msg.id
|
73
|
+
end
|
74
|
+
EOT
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns true if this mailing list's address is the canonical address of a
|
78
|
+
# User.
|
79
|
+
def canonical_address?
|
80
|
+
id == user.mailinglist_id
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns true if the address +addr+ is subscribed to this mailing list.
|
84
|
+
def has_address? addr
|
85
|
+
expand_addresses.map { |a| a.address }.member? addr
|
86
|
+
end
|
87
|
+
|
88
|
+
def before_save
|
89
|
+
welcome_admin_message ||= AdminMessage.find 1
|
90
|
+
confirmed_admin_message ||= AdminMessage.find 2
|
91
|
+
sayonara_admin_message ||= AdminMessage.find 3
|
92
|
+
end
|
93
|
+
|
94
|
+
#------------------------------------------------------------------------
|
95
|
+
# Passthroughs to the mailinglist_class
|
96
|
+
#------------------------------------------------------------------------
|
97
|
+
[ :public?, :closed?, :moderated?,
|
98
|
+
:confirmation?, :joinable?, :archived?, :proxify? ].each do |m|
|
99
|
+
define_method m do
|
100
|
+
mailinglist_class.send m
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
#------------------------------------------------------------------------
|
105
|
+
# These are all to make it work with rumble (as a replacement for
|
106
|
+
# rumble's mailing-list class).
|
107
|
+
#------------------------------------------------------------------------
|
108
|
+
|
109
|
+
def address; "#{name}@#{user.domain.name}"; end
|
110
|
+
def bounceaddress; "#{name}-bounce@#{user.domain.name}"; end
|
111
|
+
def requestaddress; "#{name}-request@#{user.domain.name}"; end
|
112
|
+
|
113
|
+
def self.find_by_address addr
|
114
|
+
if ml = find_by_basic_address(addr) then
|
115
|
+
return [ml,:mail]
|
116
|
+
elsif info = find_by_proxy_address(addr) then
|
117
|
+
ml, address_id = info
|
118
|
+
return [ml, :proxy, address_id]
|
119
|
+
elsif(ml = find_by_bounces_address(addr)) then
|
120
|
+
return [ml, :bounces]
|
121
|
+
elsif(ml = find_by_request_address(addr)) then
|
122
|
+
return [ml, :request]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.find_by_basic_address addr
|
127
|
+
(ml_name, domain_name) = addr.split "@"
|
128
|
+
domain=Domain.find_by_name domain_name
|
129
|
+
if domain
|
130
|
+
ml=Mailinglist.find_all_by_name(ml_name).find do |ml|
|
131
|
+
ml.domain.id == domain.id
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def self.find_by_proxy_address addr
|
137
|
+
if matchdata=addr.match(/-([0-9]+)\@/) then
|
138
|
+
proxy_id=matchdata[1].to_i
|
139
|
+
if ml=find_by_basic_address(addr.sub(/-[0-9]+\@/,"@")) then
|
140
|
+
begin
|
141
|
+
proxy=ProxyLink.find(proxy_id)
|
142
|
+
return ml, proxy.address
|
143
|
+
rescue
|
144
|
+
nil
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def self.find_by_bounces_address bounces_addr
|
151
|
+
find_by_basic_address bounces_addr.sub(/-bounces\@/,"@")
|
152
|
+
end
|
153
|
+
|
154
|
+
def self.find_by_request_address request_addr
|
155
|
+
find_by_basic_address request_addr.sub(/-request\@/,"@")
|
156
|
+
end
|
157
|
+
|
158
|
+
def confirmed? addr
|
159
|
+
if confirmation? then
|
160
|
+
Confirmationcode.confirmed? self.id, addr.id
|
161
|
+
else
|
162
|
+
true
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def confirmed_addresses
|
167
|
+
addresses.find_all { |addr| confirmed? addr }
|
168
|
+
end
|
169
|
+
|
170
|
+
def pending_addresses
|
171
|
+
addresses.find_all { |addr| not confirmed? addr }
|
172
|
+
end
|
173
|
+
|
174
|
+
alias unconfirmed_addresses pending_addresses
|
175
|
+
|
176
|
+
def expand_addresses
|
177
|
+
address_list = addresses
|
178
|
+
expanded = false
|
179
|
+
until expanded
|
180
|
+
foundsome = false
|
181
|
+
new_address_list = address_list.map do |addr|
|
182
|
+
if ml = Mailinglist.find_by_address(addr.address) then
|
183
|
+
foundsome = true
|
184
|
+
ml[0].addresses
|
185
|
+
else
|
186
|
+
addr
|
187
|
+
end
|
188
|
+
end.flatten
|
189
|
+
expanded = ! foundsome
|
190
|
+
address_list = new_address_list
|
191
|
+
end
|
192
|
+
address_list
|
193
|
+
end
|
194
|
+
|
195
|
+
def domain; user.domain; end
|
196
|
+
|
197
|
+
def store_message message; messages << message; end
|
198
|
+
|
199
|
+
def send_message message
|
200
|
+
message.deliver
|
201
|
+
end
|
202
|
+
|
203
|
+
def subscribe(address,confirmationcode = nil)
|
204
|
+
addr=Address.find_or_create_by_address(address)
|
205
|
+
if confirmation?
|
206
|
+
if confirmed_addresses.member? addr
|
207
|
+
true
|
208
|
+
else
|
209
|
+
if confirmationcode then
|
210
|
+
confirm addr, confirmationcode
|
211
|
+
else
|
212
|
+
send_welcome_message(addr)
|
213
|
+
addresses << addr
|
214
|
+
end
|
215
|
+
end
|
216
|
+
else
|
217
|
+
if addresses.member? addr
|
218
|
+
true
|
219
|
+
else
|
220
|
+
addresses << addr
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def send_welcome_message(address)
|
226
|
+
if Address === address then
|
227
|
+
addr = address
|
228
|
+
else
|
229
|
+
addr=Address.find_or_create_by_address(address)
|
230
|
+
end
|
231
|
+
c=Confirmationcode.find_by_address_id_and_mailinglist_id addr.id, id
|
232
|
+
unless c
|
233
|
+
c=Confirmationcode.new
|
234
|
+
c.address_id=addr.id
|
235
|
+
c.mailinglist_id=id
|
236
|
+
c.save
|
237
|
+
end
|
238
|
+
|
239
|
+
unless c.confirmed?
|
240
|
+
welcome_admin_message.send_mail bounceaddress, addr.address,
|
241
|
+
:address => addr.address,
|
242
|
+
:command => "subscribe #{addr.address} #{c.code}",
|
243
|
+
:domain => domain.name,
|
244
|
+
:name => name,
|
245
|
+
:description => description,
|
246
|
+
:requestaddress => requestaddress
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def confirm address,confirmationcode
|
251
|
+
addr=nil
|
252
|
+
|
253
|
+
if Address === address
|
254
|
+
addr=address
|
255
|
+
else
|
256
|
+
addr=Address.find_by_address(address)
|
257
|
+
end
|
258
|
+
|
259
|
+
if addr then
|
260
|
+
if confirmation? then
|
261
|
+
if Confirmationcode.confirm(self, addr, confirmationcode) then
|
262
|
+
save
|
263
|
+
confirmed_admin_message.send_mail bounceaddress,
|
264
|
+
addr.address,
|
265
|
+
:address => addr.address,
|
266
|
+
:domain => domain.name,
|
267
|
+
:name => name,
|
268
|
+
:description => description,
|
269
|
+
:requestaddress => requestaddress
|
270
|
+
return true
|
271
|
+
else
|
272
|
+
return false
|
273
|
+
end
|
274
|
+
else
|
275
|
+
return true
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def unsubscribe(address, send_mail = true)
|
281
|
+
addr=nil
|
282
|
+
|
283
|
+
unless Address === address
|
284
|
+
addr=Address.find_by_address address
|
285
|
+
else
|
286
|
+
addr=address
|
287
|
+
end
|
288
|
+
|
289
|
+
if addresses.member? addr then
|
290
|
+
if send_mail
|
291
|
+
sayonara_admin_message.send_mail bounceaddress,
|
292
|
+
addr.address,
|
293
|
+
:address => addr.address,
|
294
|
+
:domain => domain.name,
|
295
|
+
:name => name,
|
296
|
+
:description => description,
|
297
|
+
:requestaddress => requestaddress
|
298
|
+
end
|
299
|
+
remove_addresses addr
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
def handle_request requestmessage
|
304
|
+
subject=requestmessage.headers["Subject"][0].contents
|
305
|
+
matches=requestmessage.body.match(/^\W*subscribe\s+(\S+@[a-z0-9.-]+)
|
306
|
+
(?:\s+([a-z0-9]{16}))?$/x)
|
307
|
+
if matches then
|
308
|
+
# puts "Found subscription request: #{matches[0]}"
|
309
|
+
|
310
|
+
addresscandidate=matches[1]
|
311
|
+
confirmationcode=matches[2] # returns nil if there isn'tone
|
312
|
+
|
313
|
+
# Since you can have mailing lists that require confirmation, but
|
314
|
+
# they're not joinable, check for the confirmationcode case first.
|
315
|
+
if confirmationcode then
|
316
|
+
return subscribe(addresscandidate, confirmationcode)
|
317
|
+
else
|
318
|
+
if joinable? then
|
319
|
+
return subscribe(addresscandidate)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
# TODO: actually implement confirmed unsubscribing down in the
|
325
|
+
# model. (This is, of course, to prevent pranksters from
|
326
|
+
# unsubscribing you from a mailing list behind your back.)
|
327
|
+
matches=requesmessage.body.match(/^\W*unsubscribe\s+(\S+@[a-z0-9.-]+)
|
328
|
+
(?:\s+([a-z0-9]{16}))?$/x)
|
329
|
+
if matches then
|
330
|
+
addresscandidate=matches[1]
|
331
|
+
confirmationcode=matches[2]
|
332
|
+
|
333
|
+
return unsubscribe(addresscandidate, confirmationcode)
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# The mailing classes table is the table defining the behaviour of the
|
2
|
+
# mailing lists, and is, in a way, the heart of how sugoi-mail works.
|
3
|
+
#
|
4
|
+
# Rather than simply making a distinction between, say, email-address
|
5
|
+
# forwarding (the service offered by pobox.com) and mailing list
|
6
|
+
# management, everything in Sugoi-Mail is a mailing list. What _kind_
|
7
|
+
# of mailing list it is, is defined in the mailing list classes table.
|
8
|
+
#
|
9
|
+
# Delivery Attributes
|
10
|
+
# +public+:: Whether the list is open to the public or not.
|
11
|
+
# true: anyone can post to it (spammy!)
|
12
|
+
# false: only members can post to it <-- DEFAULT
|
13
|
+
# +closed+:: Determines who can send mail to the mailing list
|
14
|
+
# true: only the owner can post to it
|
15
|
+
# false: other people can post to it <-- DEFAULT
|
16
|
+
# +moderated+:: Determines whether mail goes straight to all addressees,
|
17
|
+
# or needs to be approved by the owner first.
|
18
|
+
# true: posted messages go to the owner first
|
19
|
+
# false: posted messages are broadcast <-- DEFAULT
|
20
|
+
#
|
21
|
+
# Address List Editing Attributes
|
22
|
+
# +confirmation+:: Whether subscription requests need to be confirmed,
|
23
|
+
# via an opt-in mechanism or not.
|
24
|
+
# true: subscriptions have to be confirmed <-- DEFAULT
|
25
|
+
# false: subscriptions happen automatically
|
26
|
+
# joinable:: Whether the general public can join a mailing list or not.
|
27
|
+
# If not, then only the owner can edit the address list.
|
28
|
+
# true: people can subscribe to the list at will <- DEFAULT
|
29
|
+
# false: only the owner can subscribe or unsubscribe people
|
30
|
+
#
|
31
|
+
# Other Attributes
|
32
|
+
# +archived+:: Whether messages are retained after delivery or not.
|
33
|
+
# true: Messages will be stored after they're delivered (useful for
|
34
|
+
# mailing list archives online)
|
35
|
+
# false: Messages will be discarded after delivery (probably what
|
36
|
+
# people who want to use sugoi-mail for forwarding want)
|
37
|
+
class MailinglistClass < ActiveRecord::Base
|
38
|
+
has_many :mailinglists
|
39
|
+
end
|