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 +23 -0
- data/Rakefile +1 -1
- data/VERSION.yml +1 -1
- data/lib/mlist.rb +20 -1
- data/lib/mlist/email.rb +75 -0
- data/lib/mlist/email_post.rb +105 -0
- data/lib/mlist/email_server.rb +1 -2
- data/lib/mlist/email_server/base.rb +7 -4
- data/lib/mlist/email_server/default.rb +31 -0
- data/lib/mlist/email_server/fake.rb +2 -2
- data/lib/mlist/email_server/pop.rb +28 -0
- data/lib/mlist/email_server/smtp.rb +21 -0
- data/lib/mlist/email_subscriber.rb +6 -0
- data/lib/mlist/list.rb +38 -7
- data/lib/mlist/mail_list.rb +125 -54
- data/lib/mlist/manager/database.rb +8 -4
- data/lib/mlist/message.rb +78 -74
- data/lib/mlist/server.rb +12 -4
- data/lib/mlist/thread.rb +26 -2
- data/lib/mlist/util.rb +3 -0
- data/lib/mlist/util/email_helpers.rb +53 -0
- data/lib/mlist/util/header_sanitizer.rb +3 -0
- data/lib/mlist/util/tmail_builder.rb +42 -0
- data/lib/mlist/util/tmail_methods.rb +93 -0
- data/lib/pop_ssl.rb +999 -0
- metadata +12 -3
- data/lib/mlist/email_server/email.rb +0 -47
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
|
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']
|
data/VERSION.yml
CHANGED
data/lib/mlist.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/mlist/email.rb
ADDED
@@ -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
|
data/lib/mlist/email_server.rb
CHANGED
@@ -1,17 +1,20 @@
|
|
1
1
|
module MList
|
2
2
|
module EmailServer
|
3
3
|
class Base
|
4
|
-
|
4
|
+
attr_reader :settings
|
5
|
+
|
6
|
+
def initialize(settings)
|
7
|
+
@settings = settings
|
5
8
|
@receivers = []
|
6
9
|
end
|
7
10
|
|
8
|
-
def deliver(
|
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 =
|
14
|
-
@receivers.each { |r| r.
|
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
|
@@ -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
|
data/lib/mlist/list.rb
CHANGED
@@ -6,8 +6,11 @@ module MList
|
|
6
6
|
# into may re-define behavior appropriately.
|
7
7
|
#
|
8
8
|
module List
|
9
|
-
|
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
|
-
|
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(
|
42
|
-
|
44
|
+
def recipients(subscriber)
|
45
|
+
subscribers.collect(&:email_address) - [subscriber.email_address]
|
43
46
|
end
|
44
47
|
|
45
|
-
def subscriber
|
46
|
-
|
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
|