aiwilliams-mlist 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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