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 +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
|