mlist 0.1.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/CHANGELOG +59 -0
  2. data/README +204 -0
  3. data/Rakefile +27 -0
  4. data/TODO +36 -0
  5. data/VERSION.yml +4 -0
  6. data/lib/mlist/email.rb +69 -0
  7. data/lib/mlist/email_post.rb +126 -0
  8. data/lib/mlist/email_server/base.rb +33 -0
  9. data/lib/mlist/email_server/default.rb +31 -0
  10. data/lib/mlist/email_server/fake.rb +16 -0
  11. data/lib/mlist/email_server/pop.rb +28 -0
  12. data/lib/mlist/email_server/smtp.rb +24 -0
  13. data/lib/mlist/email_server.rb +2 -0
  14. data/lib/mlist/email_subscriber.rb +6 -0
  15. data/lib/mlist/list.rb +183 -0
  16. data/lib/mlist/mail_list.rb +277 -0
  17. data/lib/mlist/manager/database.rb +48 -0
  18. data/lib/mlist/manager/notifier.rb +31 -0
  19. data/lib/mlist/manager.rb +30 -0
  20. data/lib/mlist/message.rb +150 -0
  21. data/lib/mlist/server.rb +62 -0
  22. data/lib/mlist/thread.rb +98 -0
  23. data/lib/mlist/util/email_helpers.rb +155 -0
  24. data/lib/mlist/util/header_sanitizer.rb +71 -0
  25. data/lib/mlist/util/quoting.rb +70 -0
  26. data/lib/mlist/util/tmail_builder.rb +42 -0
  27. data/lib/mlist/util/tmail_methods.rb +138 -0
  28. data/lib/mlist/util.rb +12 -0
  29. data/lib/mlist.rb +46 -0
  30. data/lib/pop_ssl.rb +999 -0
  31. data/rails/init.rb +22 -0
  32. data/spec/fixtures/schema.rb +94 -0
  33. data/spec/integration/date_formats_spec.rb +12 -0
  34. data/spec/integration/mlist_spec.rb +232 -0
  35. data/spec/integration/pop_email_server_spec.rb +22 -0
  36. data/spec/integration/proof_spec.rb +74 -0
  37. data/spec/matchers/equal_tmail.rb +53 -0
  38. data/spec/matchers/have_address.rb +48 -0
  39. data/spec/matchers/have_header.rb +104 -0
  40. data/spec/models/email_post_spec.rb +100 -0
  41. data/spec/models/email_server/base_spec.rb +11 -0
  42. data/spec/models/email_spec.rb +54 -0
  43. data/spec/models/mail_list_spec.rb +469 -0
  44. data/spec/models/message_spec.rb +109 -0
  45. data/spec/models/thread_spec.rb +83 -0
  46. data/spec/models/util/email_helpers_spec.rb +47 -0
  47. data/spec/models/util/header_sanitizer_spec.rb +19 -0
  48. data/spec/models/util/quoting_spec.rb +96 -0
  49. data/spec/spec_helper.rb +76 -0
  50. metadata +103 -0
data/CHANGELOG ADDED
@@ -0,0 +1,59 @@
1
+ *0.1.9 [Enhancement] (2009-12-21)
2
+
3
+ * DomainKey-Signature and DKIM-Signature headers will not be published to list so that sending SMTP servers may sign. [aiwilliams]
4
+ * Deploying to http://gemcutter.org [aiwilliams]
5
+
6
+ *0.1.8 [Enhancement] (2009-08-19)
7
+
8
+ * return-receipt-to header will not be published to list to avoid having recipients spam list with receipts [aiwilliams]
9
+
10
+ *0.1.7 [Bug Fixes, Delivery Improvements] (2009-08-14)
11
+
12
+ *   is maintained as spacing in html_to_text conversions [aiwilliams]
13
+ * Fixed bug where delivered email included the original Cc header. This could cause all kinds of problems. [aiwilliams]
14
+ * Fixed bug where list addresses in Cc header were not be utilized in determining the lists to deliver to. [aiwilliams]
15
+ * Fixed bug where delivery date was left to the mercy of the Rails time configuration. Using Time.now. [aiwilliams]
16
+ * Manager list can indicate if reply-to should be list address or subscriber. [aiwilliams]
17
+ * Leaving the reply-to field intact when it already exists on incoming email. [aiwilliams]
18
+ * Messages will not be delivered to addresses found in the TO and CC fields in order to keep those recipients from getting two emails. [aiwilliams]
19
+ * Added ability to optionally copy sender. [aiwilliams]
20
+
21
+ *0.1.6 [Bug Fixes] (March 5, 2009)
22
+
23
+ * Messages are processed even when there are no recipients [aiwilliams]
24
+ * Escaping DQUOTE and \ in email address phrase for some headers [aiwilliams]
25
+
26
+ *0.1.5 [Solid Basics] (January 17, 2009)
27
+
28
+ * No longer storing the list label in the message subject field, thereby supporting cleaner viewing of threads [aiwilliams]
29
+ * Improved handling of incoming email subjects by leaving labels alone that aren't obviously the label of the list, thereby allowing for subjects like "[Ann] My New Thing" [aiwilliams]
30
+ * Notifying subscribers when the list indicates that they are currently blocked from posting messages to a list they are subscribed to [aiwilliams]
31
+ * Added MList::Manager module to better define the interface of list managers. This needs to be included into list manager implementations. [aiwilliams]
32
+ * MList::List implementors may now answer the footer content to be appended to the bottom of the text/plain part of messages. [aiwilliams]
33
+ * List footers are stripped from text/plain part of messages before being delivered. [aiwilliams]
34
+ * Observers of MList models which are defined in client applications now work without special instruction. [aiwilliams]
35
+ * A first pass implementation for converting html to text using Hpricot. [aiwilliams]
36
+ * Better thread tree, supporting message navigation within a thread through a linked list kind of approach. [aiwilliams]
37
+ * Better parent message associating using in-reply-to, then references, then subject. [aiwilliams]
38
+ * MList.version is hash of {:major => 0, :minor => 0, :patch => 0}, with a to_s of 'MList 0.0.0'. [aiwilliams]
39
+ * Fixed bug where original email source content was last in TMail::Mail#to_s usage. [aiwilliams]
40
+
41
+ *0.1.4 [] (January 7, 2009)
42
+
43
+ * Fixed bug where default email server was not allowing for settings [aiwilliams]
44
+ * Made subject for reply place 're:' in front of the list label [aiwilliams]
45
+ * Added really simple tree support. Really simple. [aiwilliams]
46
+
47
+ *0.1.3 [] (January 7, 2009)
48
+
49
+ * Generating message id as UUID [aiwilliams]
50
+ * Allowing setting of domain for message id [aiwilliams]
51
+ * Fixed bug in storing message id [aiwilliams]
52
+
53
+ *0.1.2 [] (January 7, 2009)
54
+
55
+ * Added references header when creating a reply post. [aiwilliams]
56
+ * Including display_name in from address of EmailPost. [aiwilliams]
57
+ * Improved extraction of text when it is nested inside parts. [aiwilliams]
58
+
59
+ *0.1.1 [First Working Release] (January 5, 2009)
data/README ADDED
@@ -0,0 +1,204 @@
1
+ = MList
2
+
3
+ == DESCRIPTION:
4
+
5
+ An insane attempt to build a mail list server library that can be used in other
6
+ applications very easily. The first target is Ruby applications that can load
7
+ MList models for direct, embedded integration. It will later have a RESTful API
8
+ so that any language/architecture can be easily integrated. That may depend
9
+ heavily on community involvement...
10
+
11
+ == FEATURES/PROBLEMS:
12
+
13
+ If you have any experience with mailing lists, things can be understood most
14
+ quickly with this: MList is the mailing list server and database, your
15
+ application is the list manager.
16
+
17
+ MList is being used in a production environment as a Ruby gem/Rails plugin. I
18
+ decided not to use other list software primarily because a) we already had a
19
+ running, sophisticated list manager that I didn't care to synchronize with
20
+ something else, b) we have an exceptional UI design for listing messages in
21
+ threads and ultimately will have search and c) integration options for the other
22
+ software looked to be a problem in themselves.
23
+
24
+ And then there's the fame and glory that comes with building something like
25
+ this...
26
+
27
+ There is a LOT to do: segmenting, i18n, backscatter - only the Mailman developers
28
+ know what else. I have enough experience to know that rewrites are NEVER as easy
29
+ as they seem. Alas, I go boldly forward.
30
+
31
+ ==== Thread Trees
32
+
33
+ A Thread answers a tree of messages where each message in the tree knows it's
34
+ parent, children, previous and next message, and whether it is the root or a leaf
35
+ in the tree. The messages are actually delegates, wrappers around the real
36
+ message instances from the thread.
37
+
38
+ This approach makes a few assumptions:
39
+
40
+ # You want the tree because you will be displaying it # Moving through a tree
41
+ message by message should 'feel' right; next is determined by walking the tree
42
+ depth first.
43
+
44
+ It may or may not prove useful someday to have this knowledge in the form of
45
+ something like awesome_nested_set.
46
+
47
+ ==== Extracting 'from' IP address from emails is not implemented
48
+
49
+ http://compnetworking.about.com/od/workingwithipaddresses/qt/ipaddressemail.htm
50
+
51
+ ==== Deleting messages
52
+
53
+ Mail list servers don't typically deal with this, do they? When an email is sent,
54
+ it's sent! If you delete one, how can a reply to it be accurately injected into a
55
+ partially broken tree?
56
+
57
+ Since MList is designed to integrate with applications that want to distribute
58
+ messages, those applications may want to delete messages. I feel, at the time of
59
+ writing this, that the feature of deleting is NOT a problem that MList should
60
+ attempt to address in great detail. The models are ActiveRecord descendants, so
61
+ they can be deleted with #destroy, et. al. MList will not prevent that from
62
+ happening. This has implications, of course.
63
+
64
+ # MList::Thread#tree will likely break if messages are gone.
65
+
66
+ In my own usage, I have opted for adding a column to my mlist_messages table,
67
+ deleted_at. The application will make decisions based on whether that column has
68
+ a value or not. I would suggest you implement something similar, always favoring
69
+ leaving the records in place (paranoid delete). Since MList::MailList#messages
70
+ (and other such has_many associations) don't know about this column, they will
71
+ always answer collections which still include those messages.
72
+
73
+ When an MList::MailList is destroyed, all it's MList::Messages and MList::Threads
74
+ will be deleted (:dependent => :delete_all). If no other MList::Messages are
75
+ referencing the MList::Email records, they will also be deleted.
76
+
77
+ ==== Ensuring Delivery
78
+
79
+ The internet email system is a mess. Spam has proven to be a significant
80
+ problem which plagues both your inbox and the service providers who work to
81
+ deliver email into it. So, what must you, the user of MList do, to ensure that
82
+ your software plays nice, and your emails are delivered?
83
+
84
+ Of course, this problem goes well beyond MList - any email you send from your
85
+ application servers will suffer the consequences of a misconfigured domain.
86
+ Here's what I've learned:
87
+
88
+ * Go to http://old.openspf.org/wizard.html and create your SPF record
89
+ content, to be placed in a TXT record for your domain.
90
+ - If you are using Google Apps, check this out:
91
+ http://www.google.com/support/a/bin/answer.py?hl=en&answer=33786
92
+ * Make sure you have an abuse@yourdomain.com and postmaster@yourdomain.com
93
+ address.
94
+ - If you are using Google Apps, you will need to create 'groups' for those
95
+ addresses, adding yourself as a recipient. You can read more about it
96
+ here: http://blog.wordtothewise.com/2009/01/google-apps-wheres-my-abuse/
97
+ * Be prepared to visit the Feedback Loop (FBL) configuration pages of many
98
+ ISPs, like the one at AOL: http://postmaster.aol.com/fbl/
99
+ * You'll probably want to have a way to resolve an email address (subscriber)
100
+ to an IP address from which the subscriber subscribed. This will help your
101
+ case should you have an ISP rejecting your mail.
102
+
103
+ There is a great article here:
104
+
105
+ http://community-support.engineyard.com/faqs/guides/making-sure-your-email-gets-delivered
106
+
107
+ Although that is EY focused, it gives you a good idea of some of the things
108
+ you should get right.
109
+
110
+ == SYNOPSIS:
111
+
112
+ Let's say you want your web application to have a mailing list feature. Let's
113
+ also say you care about the UI, and you don't want to learn all about creating
114
+ the correct HTML structures for a mailing list. You want to have lots of power
115
+ for searching the mail, and you have your own strategy for managing the lists.
116
+ You love Ruby. You want MList.
117
+
118
+ == REQUIREMENTS:
119
+
120
+ You'll need some gems.
121
+
122
+ * hpricot
123
+ * uuid (macaddr also)
124
+ * tmail
125
+ * active_support
126
+ * active_record
127
+
128
+ == INSTALL:
129
+
130
+ - gem sources add http://gemcutter.org
131
+ - sudo gem install mlist
132
+
133
+ For now, copy the mlist_ tables from the spec/fixtures/schema.rb file and move
134
+ them to a new migration in your application. Run the migration.
135
+
136
+ Now you'll need to create the MList::Server and provide it with a list manager
137
+ and MList::EmailServer::Default instance. Something like this in your
138
+ environment.rb after the initialize block (our gem needs to have been loaded):
139
+
140
+ Rails::Initializer.run do |config|
141
+ # Please do specify a version, and check for the latest!
142
+ config.gem "mlist", :version => '0.1.0'
143
+ end
144
+
145
+ MLIST_SERVER = MList::Server.new(
146
+ :list_manager => MList::Manager::Database.new,
147
+ :email_server => MList::EmailServer::Default.new(
148
+ MList::EmailServer::Pop.new(
149
+ :ssl => false,
150
+ :server => 'pop.gmail.com',
151
+ :port => '995',
152
+ :username => 'yourusername',
153
+ :password => 'yourpassword'
154
+ ),
155
+ MList::EmailServer::Smtp.new(
156
+ ActionMailer::Base.smtp_settings # probably good enough!
157
+ )
158
+ )
159
+ )
160
+
161
+ Your list manager needs to implement only two methods. Check out (and use if you
162
+ like) the MList::Manager::Database for more information.
163
+
164
+ You'll need something to trigger the incoming server process. Take your pick from
165
+ http://wiki.rubyonrails.org/rails/pages/HowToRunBackgroundJobsInRails. In the
166
+ end, if you don't write your own incoming server thing and go with the POP GMail
167
+ example above, your background process will "MLIST_SERVER.email_server.execute".
168
+
169
+ Take a look at MList::EmailPost if you're building a UI. It can be posted to a
170
+ list something like this:
171
+
172
+ class MyList # instances of these are given by your list manager
173
+ def post(attributes)
174
+ email = MList::EmailPost.new({
175
+ :mailer => 'MyApplication'
176
+ }.merge(attributes))
177
+ raise 'You can validate these' unless email.valid?
178
+ message = MLIST_SERVER.mail_list(self).post(email)
179
+ # do what you will with message. it's already saved.
180
+ end
181
+ end
182
+
183
+ == LICENSE:
184
+
185
+ (The MIT License)
186
+
187
+ Copyright (c) 2008-2009 Adam Williams (aiwilliams)
188
+
189
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
190
+ this software and associated documentation files (the 'Software'), to deal in the
191
+ Software without restriction, including without limitation the rights to use,
192
+ copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
193
+ Software, and to permit persons to whom the Software is furnished to do so,
194
+ subject to the following conditions:
195
+
196
+ The above copyright notice and this permission notice shall be included in all
197
+ copies or substantial portions of the Software.
198
+
199
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
200
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
201
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
202
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
203
+ AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
204
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), 'lib'))
2
+
3
+ require 'rubygems'
4
+ require 'spec/rake/spectask'
5
+
6
+ task :default => :spec
7
+
8
+ desc "Run all specs"
9
+ Spec::Rake::SpecTask.new do |t|
10
+ t.spec_files = FileList['spec/**/*_spec.rb']
11
+ t.spec_opts = ['--options', 'spec/spec.opts']
12
+ end
13
+
14
+ begin
15
+ require 'jeweler'
16
+ Jeweler::Tasks.new do |s|
17
+ s.name = 'mlist'
18
+ s.summary = 'A Ruby mailing list library designed to be integrated into other applications.'
19
+ s.email = 'adam@thewilliams.ws'
20
+ s.files = FileList["[A-Z]*", "{lib,rails}/**/*"].exclude("tmp")
21
+ s.homepage = "http://github.com/aiwilliams/mlist"
22
+ s.description = s.summary
23
+ s.authors = ['Adam Williams']
24
+ end
25
+ rescue LoadError
26
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
27
+ end
data/TODO ADDED
@@ -0,0 +1,36 @@
1
+ #html_to_text
2
+
3
+ ADDRESS - Address
4
+ BLOCKQUOTE - Block quotation
5
+
6
+ DIV - Generic block-level container
7
+ DL - Definition list
8
+ FIELDSET - Form control group
9
+ FORM - Interactive form
10
+ H1 - Level-one heading
11
+ H2 - Level-two heading
12
+ H3 - Level-three heading
13
+ H4 - Level-four heading
14
+ H5 - Level-five heading
15
+ H6 - Level-six heading
16
+ HR - Horizontal rule
17
+ OL - Ordered list
18
+ P - Paragraph
19
+ PRE - Preformatted text
20
+
21
+ TABLE - Table
22
+ output TRs as lines, csv the TDs
23
+
24
+ UL - Unordered list
25
+
26
+ DD - Definition description
27
+ DT - Definition term
28
+ LI - List item
29
+
30
+
31
+ a (Links) - Link to Somewhere[1] ---- [1] http://
32
+ b (Bold) - *bold*
33
+ i (Italic) - _italic_
34
+ strong - *strong*
35
+ em (emphasis) - _emphasis_
36
+ u (underline) - probably do nothing, maybe something like em
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 9
3
+ :major: 0
4
+ :minor: 1
@@ -0,0 +1,69 @@
1
+ module MList
2
+
3
+ class Email < ActiveRecord::Base
4
+ set_table_name 'mlist_emails'
5
+
6
+ include MList::Util::EmailHelpers
7
+ include MList::Util::TMailReaders
8
+
9
+ def been_here?(list)
10
+ tmail.header_string('x-beenthere') == list.address
11
+ end
12
+
13
+ def date
14
+ if date_from_email = super
15
+ return date_from_email
16
+ else
17
+ self.created_at ||= Time.now
18
+ end
19
+ end
20
+
21
+ def from
22
+ tmail.header_string('from')
23
+ end
24
+
25
+ # Answers the usable destination addresses of the email.
26
+ #
27
+ def list_addresses
28
+ bounce? ? tmail.header_string('to').match(/\Amlist-(.*)\Z/)[1] : recipient_addresses
29
+ end
30
+
31
+ # Answers true if this email is a bounce.
32
+ #
33
+ # TODO Delegate to the email_server's bounce detector.
34
+ #
35
+ def bounce?
36
+ tmail.header_string('to') =~ /mlist-/
37
+ end
38
+
39
+ def tmail=(tmail)
40
+ @tmail = tmail
41
+ write_attribute(:source, tmail.port.read_all)
42
+ end
43
+
44
+ def tmail
45
+ @tmail ||= TMail::Mail.parse(source)
46
+ end
47
+
48
+ # Provide reader delegation to *most* of the underlying TMail::Mail
49
+ # methods, excluding those overridden by this Class and the [] method (an
50
+ # ActiveRecord method).
51
+ def method_missing(symbol, *args, &block) # :nodoc:
52
+ if symbol.to_s !~ /=\Z/ && symbol != :[] && symbol != :source && tmail.respond_to?(symbol)
53
+ tmail.__send__(symbol, *args, &block)
54
+ else
55
+ super
56
+ end
57
+ end
58
+
59
+ # Answers the set of addresses found in the TO and CC fields of the email.
60
+ #
61
+ def recipient_addresses
62
+ (Array(tmail.to) + Array(tmail.cc)).collect(&:downcase).uniq
63
+ end
64
+
65
+ def respond_to?(method)
66
+ super || (method.to_s !~ /=\Z/ && tmail.respond_to?(method))
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,126 @@
1
+ module MList
2
+
3
+ # The simplest post that can be made to an MList::MailList. Every instance
4
+ # must have at least the text content and a subject. Html may also be added.
5
+ #
6
+ # It is important to understand that this class is intended to be used by
7
+ # applications that have some kind of UI for creating a post. It assumes
8
+ # Rails form builder support is desired, and that there is no need for
9
+ # manipulating the final TMail::Mail object that will be delivered to the
10
+ # list outside of the methods provided herein.
11
+ #
12
+ class EmailPost
13
+ include MList::Util::EmailHelpers
14
+
15
+ ATTRIBUTE_NAMES = %w(copy_sender html text mailer subject subscriber)
16
+ ATTRIBUTE_NAMES.each do |attribute_name|
17
+ define_method(attribute_name) do
18
+ @attributes[attribute_name]
19
+ end
20
+ define_method("#{attribute_name}=") do |value|
21
+ @attributes[attribute_name] = value
22
+ end
23
+ end
24
+
25
+ attr_reader :parent_identifier, :reply_to_message
26
+
27
+ def initialize(attributes)
28
+ @attributes = {}
29
+ self.attributes = {
30
+ :mailer => 'MList Client Application'
31
+ }.merge(attributes)
32
+ end
33
+
34
+ def attributes
35
+ @attributes.dup
36
+ end
37
+
38
+ def attributes=(new_attributes)
39
+ return if new_attributes.nil?
40
+ attributes = new_attributes.dup
41
+ attributes.stringify_keys!
42
+ attributes.each do |attribute_name, value|
43
+ send("#{attribute_name}=", value)
44
+ end
45
+ end
46
+
47
+ def copy_sender=(value)
48
+ @attributes['copy_sender'] = %w(true 1).include?(value.to_s)
49
+ end
50
+
51
+ def reply_to_message=(message)
52
+ if message
53
+ @parent_identifier = message.identifier
54
+ else
55
+ @parent_identifier = nil
56
+ end
57
+ @reply_to_message = message
58
+ end
59
+
60
+ def subject
61
+ @attributes['subject'] || (reply_to_message ? "Re: #{reply_to_message.subject}" : nil)
62
+ end
63
+
64
+ def to_s
65
+ to_tmail.to_s
66
+ end
67
+
68
+ def to_tmail
69
+ raise ActiveRecord::RecordInvalid.new(self) unless valid?
70
+
71
+ builder = MList::Util::TMailBuilder.new(TMail::Mail.new)
72
+
73
+ builder.mime_version = "1.0"
74
+ builder.mailer = mailer
75
+
76
+ if parent_identifier
77
+ builder.in_reply_to = parent_identifier
78
+ builder.references = [bracket(parent_identifier)]
79
+ end
80
+
81
+ builder.from = subscriber_name_and_address(subscriber)
82
+ builder.subject = subject
83
+
84
+ if html
85
+ builder.add_text_part(text)
86
+ builder.add_html_part(html)
87
+ builder.set_content_type('multipart/alternative')
88
+ else
89
+ builder.body = text
90
+ builder.set_content_type('text/plain')
91
+ end
92
+
93
+ builder.tmail
94
+ end
95
+
96
+ # vvv ActiveRecord validations interface implementation vvv
97
+
98
+ def self.human_name(options = {})
99
+ self.name.humanize
100
+ end
101
+
102
+ def self.self_and_descendants_from_active_record #nodoc:
103
+ [self]
104
+ end
105
+
106
+ def self.human_attribute_name(attribute_key_name, options = {})
107
+ attribute_key_name.humanize
108
+ end
109
+
110
+ def errors
111
+ @errors ||= ActiveRecord::Errors.new(self)
112
+ end
113
+
114
+ def validate
115
+ errors.clear
116
+ errors.add(:subject, 'required') if subject.blank?
117
+ errors.add(:text, 'required') if text.blank?
118
+ errors.add(:text, 'needs to be a bit longer') if !text.blank? && text.strip.size < 25
119
+ errors.empty?
120
+ end
121
+
122
+ def valid?
123
+ validate
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,33 @@
1
+ module MList
2
+ module EmailServer
3
+ class Base
4
+ attr_reader :settings
5
+
6
+ def initialize(settings)
7
+ @settings = {
8
+ :domain => ::Socket.gethostname
9
+ }.merge(settings)
10
+
11
+ @uuid = UUID.new
12
+ @receivers = []
13
+ end
14
+
15
+ def deliver(tmail)
16
+ raise 'Implement actual delivery mechanism in subclasses'
17
+ end
18
+
19
+ def generate_message_id
20
+ "#{@uuid.generate}@#{@settings[:domain]}"
21
+ end
22
+
23
+ def receive(tmail)
24
+ email = MList::Email.new(:tmail => tmail)
25
+ @receivers.each { |r| r.receive_email(email) }
26
+ end
27
+
28
+ def receiver(rx)
29
+ @receivers << rx
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,31 @@
1
+ module MList
2
+ module EmailServer
3
+
4
+ class Default < Base
5
+ def initialize(incoming_server, outgoing_server, settings = {})
6
+ super(settings)
7
+ @incoming_server, @outgoing_server = incoming_server, outgoing_server
8
+ @incoming_server.receiver(self)
9
+ end
10
+
11
+ # Delegates delivery of email to outgoing server.
12
+ #
13
+ def deliver(tmail)
14
+ @outgoing_server.deliver(tmail)
15
+ end
16
+
17
+ # Delegates fetching emails to incoming server.
18
+ def execute
19
+ @incoming_server.execute
20
+ end
21
+
22
+ # Delegates processing of email from incoming server to receivers on
23
+ # self.
24
+ #
25
+ def receive_email(email)
26
+ @receivers.each { |r| r.receive_email(email) }
27
+ end
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,16 @@
1
+ module MList
2
+ module EmailServer
3
+ class Fake < Base
4
+ attr_reader :deliveries
5
+
6
+ def initialize(settings = {})
7
+ super
8
+ @deliveries = []
9
+ end
10
+
11
+ def deliver(tmail)
12
+ @deliveries << tmail
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,28 @@
1
+ require 'pop_ssl'
2
+
3
+ module MList
4
+ module EmailServer
5
+
6
+ class Pop < Base
7
+ def deliver(tmail)
8
+ raise "Mail cannot be delivered through a POP server. Please use the '#{MList::EmailServer::Default.name}' type."
9
+ end
10
+
11
+ def execute
12
+ connect_to_email_account do |pop|
13
+ pop.mails.each { |message| receive(TMail::Mail.parse(message.pop)); message.delete }
14
+ end
15
+ end
16
+
17
+ private
18
+ def connect_to_email_account
19
+ pop3 = Net::POP3.new(settings[:server], settings[:port], false)
20
+ pop3.enable_ssl if settings[:ssl]
21
+ pop3.start(settings[:username], settings[:password]) do |pop|
22
+ yield pop
23
+ end
24
+ end
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ require 'net/smtp'
2
+
3
+ module MList
4
+ module EmailServer
5
+
6
+ class Smtp < Base
7
+ def deliver(tmail)
8
+ destinations = tmail.destinations
9
+ tmail.delete_no_send_fields
10
+ smtp = Net::SMTP.new(settings[:address], settings[:port])
11
+ smtp.enable_starttls_auto if settings[:enable_starttls_auto] && smtp.respond_to?(:enable_starttls_auto)
12
+ smtp.start(settings[:domain], settings[:user_name], settings[:password],
13
+ settings[:authentication]) do |smtp|
14
+ smtp.sendmail(tmail.encoded, tmail['sender'], destinations)
15
+ end
16
+ end
17
+
18
+ def execute
19
+ raise "Mail cannot be received through an SMTP server. Please use the '#{MList::EmailServer::Default.name}' type."
20
+ end
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,2 @@
1
+ require 'mlist/email_server/base'
2
+ require 'mlist/email_server/default'
@@ -0,0 +1,6 @@
1
+ module MList
2
+
3
+ # Represents a simple subscriber instance, wrapping an email address.
4
+ #
5
+ EmailSubscriber = Struct.new('EmailSubscriber', :email_address)
6
+ end