mlist 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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