aiwilliams-mlist 0.0.0 → 0.1.0

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.
data/README CHANGED
@@ -17,6 +17,29 @@ backscatter - only the Mailman developers know what else. I have enough
17
17
  experience to know that rewrites are NEVER as easy as they seem. I begin
18
18
  this with fear and trepidation. Alas, I go boldly forward.
19
19
 
20
+ ==== Extracting 'from' IP address from emails is not implemented
21
+
22
+ http://compnetworking.about.com/od/workingwithipaddresses/qt/ipaddressemail.htm
23
+
24
+ ==== Observing MList ActiveRecord subsclasses (ie, MList::Message, MList::Thread)
25
+
26
+ ActiveRecord observers are reloaded at each request in development mode. They
27
+ will be registered with the MList models each time. Since the MList models are
28
+ required once at initialization, there will always only be one instance of the
29
+ model class, and therefore, many instances of the observer class (all but the
30
+ most recent invalid, since they were undefined) registered with it.
31
+
32
+ There are a number of ways to solve this, the best being the one that makes things
33
+ 'just work' such that I can delete this part of the document. For now, do the
34
+ following in environment.rb:
35
+
36
+ * remove the observer from the Rails' config.active_record.observers list
37
+ * after the Rails::Initializer.run block, "require 'myobserver'"
38
+ * after that require line, "Myobserver.instance"
39
+
40
+ This will load the observer once, thereby only registering it once with the MList
41
+ class you are observing.
42
+
20
43
  == SYNOPSIS:
21
44
 
22
45
  Let's say you want your web application to have a mailing list feature.
data/Rakefile CHANGED
@@ -17,7 +17,7 @@ begin
17
17
  s.name = 'mlist'
18
18
  s.summary = 'A Ruby mailing list library designed to be integrated into other applications.'
19
19
  s.email = 'adam@thewilliams.ws'
20
- s.files = FileList["[A-Z]*", "{lib,rails}/**/*"].exclude("tmp,**/tmp")
20
+ s.files = FileList["[A-Z]*", "{lib}/**/*"].exclude("tmp,**/tmp")
21
21
  s.homepage = "http://github.com/aiwilliams/mlist"
22
22
  s.description = s.summary
23
23
  s.authors = ['Adam Williams']
@@ -1,4 +1,4 @@
1
1
  ---
2
- :minor: 0
2
+ :minor: 1
3
3
  :patch: 0
4
4
  :major: 0
@@ -1,13 +1,32 @@
1
1
  require 'tmail'
2
+ require 'activesupport'
2
3
  require 'activerecord'
3
4
 
4
5
  require 'mlist/util'
6
+ require 'mlist/email'
5
7
  require 'mlist/message'
6
8
  require 'mlist/list'
7
9
  require 'mlist/mail_list'
10
+ require 'mlist/email_post'
8
11
  require 'mlist/email_server'
12
+ require 'mlist/email_subscriber'
9
13
  require 'mlist/server'
10
14
  require 'mlist/thread'
11
15
 
12
16
  module MList
13
- end
17
+ class DoubleDeliveryError < StandardError
18
+ def initialize(message)
19
+ super("A message should never be delivered more than once. An attempt was made to deliver this message:\n#{message.inspect}")
20
+ end
21
+ end
22
+ end
23
+
24
+ Time::DATE_FORMATS[:mlist_reply_timestamp] = Date::DATE_FORMATS[:mlist_reply_timestamp] = lambda do |time|
25
+ time.strftime('%a, %b %d, %Y at %I:%M %p').sub(/0(\d,)/, '\1').sub(/0(\d:)/, '\1')
26
+ end
27
+
28
+ # In order to keep the inline images in email intact. Certainly a scary bit of
29
+ # hacking, but this is the solution out there on the internet.
30
+ TMail::HeaderField::FNAME_TO_CLASS.delete 'content-id'
31
+
32
+ TMail::Mail::ALLOW_MULTIPLE['x-beenthere'] = true
@@ -0,0 +1,75 @@
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 from
14
+ tmail.header_string('from')
15
+ end
16
+
17
+ # Answers the usable destination addresses of the email.
18
+ #
19
+ def list_addresses
20
+ bounce? ? tmail.header_string('to').match(/\Amlist-(.*)\Z/)[1] : tmail.to.collect(&:downcase)
21
+ end
22
+
23
+ # Answers true if this email is a bounce.
24
+ #
25
+ # TODO Delegate to the email_server's bounce detector.
26
+ #
27
+ def bounce?
28
+ tmail.header_string('to') =~ /mlist-/
29
+ end
30
+
31
+ # Extracts the parent message identifier from the source, using
32
+ # in-reply-to first, then references. Brackets around the identifier are
33
+ # removed.
34
+ #
35
+ # If you provide an MList::MailList, it will be searched for a message
36
+ # having the same subject as a last resort.
37
+ #
38
+ def parent_identifier(mail_list = nil)
39
+ identifier = tmail.header_string('in-reply-to') || begin
40
+ references = tmail['references']
41
+ references.ids.first if references
42
+ end
43
+
44
+ if identifier
45
+ remove_brackets(identifier)
46
+ elsif mail_list && subject =~ /(^|[^\w])re:/i
47
+ parent_message = mail_list.messages.find(:first,
48
+ :select => 'identifier',
49
+ :conditions => ['mlist_messages.subject = ?', remove_regard(subject)],
50
+ :order => 'created_at asc')
51
+ parent_message.identifier if parent_message
52
+ end
53
+ end
54
+
55
+ def tmail=(tmail)
56
+ @tmail = tmail
57
+ write_attribute(:source, tmail.to_s)
58
+ end
59
+
60
+ def tmail
61
+ @tmail ||= TMail::Mail.parse(source)
62
+ end
63
+
64
+ # Provide reader delegation to *most* of the underlying TMail::Mail
65
+ # methods, excluding those overridden by this Class and the [] method (an
66
+ # ActiveRecord method).
67
+ def method_missing(symbol, *args, &block) # :nodoc:
68
+ if symbol.to_s !~ /=\Z/ && symbol != :[] && symbol != :source && tmail.respond_to?(symbol)
69
+ tmail.__send__(symbol, *args, &block)
70
+ else
71
+ super
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,105 @@
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
+ ATTRIBUTE_NAMES = %w(html text mailer subject subscriber)
14
+ ATTRIBUTE_NAMES.each do |attribute_name|
15
+ define_method(attribute_name) do
16
+ @attributes[attribute_name]
17
+ end
18
+ define_method("#{attribute_name}=") do |value|
19
+ @attributes[attribute_name] = value
20
+ end
21
+ end
22
+
23
+ attr_reader :parent_identifier, :reply_to_message
24
+
25
+ def initialize(attributes)
26
+ @attributes = {}
27
+ self.attributes = {
28
+ :mailer => 'MList Client Application'
29
+ }.merge(attributes)
30
+ end
31
+
32
+ def attributes
33
+ @attributes.dup
34
+ end
35
+
36
+ def attributes=(new_attributes)
37
+ return if new_attributes.nil?
38
+ attributes = new_attributes.dup
39
+ attributes.stringify_keys!
40
+ attributes.each do |attribute_name, value|
41
+ send("#{attribute_name}=", value)
42
+ end
43
+ end
44
+
45
+ def reply_to_message=(message)
46
+ if message
47
+ @parent_identifier = message.identifier
48
+ else
49
+ @parent_identifier = nil
50
+ end
51
+ @reply_to_message = message
52
+ end
53
+
54
+ def subject
55
+ @attributes['subject'] || (reply_to_message ? "Re: #{reply_to_message.subject}" : nil)
56
+ end
57
+
58
+ def to_tmail
59
+ raise ActiveRecord::RecordInvalid.new(self) unless valid?
60
+
61
+ builder = MList::Util::TMailBuilder.new(TMail::Mail.new)
62
+
63
+ builder.mime_version = "1.0"
64
+ builder.mailer = mailer
65
+
66
+ builder.in_reply_to = parent_identifier if parent_identifier
67
+
68
+ builder.from = subscriber.email_address
69
+ builder.subject = subject
70
+
71
+ if html
72
+ builder.add_text_part(text)
73
+ builder.add_html_part(html)
74
+ builder.set_content_type('multipart/alternative')
75
+ else
76
+ builder.body = text
77
+ builder.set_content_type('text/plain')
78
+ end
79
+
80
+ builder.tmail
81
+ end
82
+
83
+ # vvv ActiveRecord validations interface implementation vvv
84
+
85
+ def self.human_attribute_name(attribute_key_name, options = {})
86
+ attribute_key_name.humanize
87
+ end
88
+
89
+ def errors
90
+ @errors ||= ActiveRecord::Errors.new(self)
91
+ end
92
+
93
+ def validate
94
+ errors.clear
95
+ errors.add(:subject, 'required') if subject.blank?
96
+ errors.add(:text, 'required') if text.blank?
97
+ errors.add(:text, 'needs to be a bit longer') if !text.blank? && text.strip.size < 25
98
+ errors.empty?
99
+ end
100
+
101
+ def valid?
102
+ validate
103
+ end
104
+ end
105
+ end
@@ -1,3 +1,2 @@
1
- require 'mlist/email_server/email'
2
1
  require 'mlist/email_server/base'
3
- require 'mlist/email_server/fake'
2
+ require 'mlist/email_server/default'
@@ -1,17 +1,20 @@
1
1
  module MList
2
2
  module EmailServer
3
3
  class Base
4
- def initialize
4
+ attr_reader :settings
5
+
6
+ def initialize(settings)
7
+ @settings = settings
5
8
  @receivers = []
6
9
  end
7
10
 
8
- def deliver(email)
11
+ def deliver(tmail, destinations)
9
12
  raise 'Implement actual delivery mechanism in subclasses'
10
13
  end
11
14
 
12
15
  def receive(tmail)
13
- email = EmailServer::Email.new(tmail)
14
- @receivers.each { |r| r.receive(email) }
16
+ email = MList::Email.new(:tmail => tmail)
17
+ @receivers.each { |r| r.receive_email(email) }
15
18
  end
16
19
 
17
20
  def receiver(rx)
@@ -0,0 +1,31 @@
1
+ module MList
2
+ module EmailServer
3
+
4
+ class Default < Base
5
+ def initialize(incoming_server, outgoing_server)
6
+ super({})
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, destinations)
14
+ @outgoing_server.deliver(tmail, destinations)
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
@@ -4,11 +4,11 @@ module MList
4
4
  attr_reader :deliveries
5
5
 
6
6
  def initialize
7
- super
7
+ super({})
8
8
  @deliveries = []
9
9
  end
10
10
 
11
- def deliver(tmail)
11
+ def deliver(tmail, destinations)
12
12
  @deliveries << tmail
13
13
  end
14
14
  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, destinations)
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,21 @@
1
+ require 'net/smtp'
2
+
3
+ module MList
4
+ module EmailServer
5
+
6
+ class Smtp < Base
7
+ def deliver(tmail, destinations)
8
+ sender = tmail['return-path'] || tmail.from
9
+ Net::SMTP.start(settings[:address], settings[:port], settings[:domain],
10
+ settings[:user_name], settings[:password], settings[:authentication]) do |smtp|
11
+ smtp.sendmail(tmail.encoded, sender, destinations)
12
+ end
13
+ end
14
+
15
+ def execute
16
+ raise "Mail cannot be received through an SMTP server. Please use the '#{MList::EmailServer::Default.name}' type."
17
+ end
18
+ end
19
+
20
+ end
21
+ end
@@ -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
@@ -6,8 +6,11 @@ module MList
6
6
  # into may re-define behavior appropriately.
7
7
  #
8
8
  module List
9
- def bounce(email)
10
-
9
+
10
+ # Answers whether this list is active or not.
11
+ #
12
+ def active?
13
+ true
11
14
  end
12
15
 
13
16
  def host
@@ -27,7 +30,7 @@ module MList
27
30
  end
28
31
 
29
32
  def list_id
30
- "#{label} <#{address}>"
33
+ raise 'answer a unique, never changing value'
31
34
  end
32
35
 
33
36
  def name
@@ -38,12 +41,40 @@ module MList
38
41
  address
39
42
  end
40
43
 
41
- def recipients(message)
42
- subscriptions.collect(&:address) - [message.from_address]
44
+ def recipients(subscriber)
45
+ subscribers.collect(&:email_address) - [subscriber.email_address]
43
46
  end
44
47
 
45
- def subscriber?(address)
46
- !subscriptions.detect {|s| s.address == address}.nil?
48
+ def subscriber(email_address)
49
+ subscribers.detect {|s| s.email_address == email_address}
47
50
  end
51
+
52
+ def subscriber?(email_address)
53
+ !subscriber(email_address).nil?
54
+ end
55
+
56
+ # Methods that will be invoked on your implementation of Mlist::List when
57
+ # certain events occur during the processing of email sent to a list.
58
+ #
59
+ module Callbacks
60
+ def bounce(email)
61
+ end
62
+
63
+ # Called when an email is a post to the list while the list is inactive
64
+ # (answers false to _active?_). This will not be called if the email is
65
+ # from a non-subscribed sender. Instead, _non_subscriber_post_ will be
66
+ # called.
67
+ #
68
+ def inactive_post(email)
69
+ end
70
+
71
+ # Called when an email is a post to the list from a non-subscribed
72
+ # sender. This will be called even if the list is inactive.
73
+ #
74
+ def non_subscriber_post(email)
75
+ end
76
+ end
77
+ include Callbacks
78
+
48
79
  end
49
80
  end