aiwilliams-mlist 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +63 -0
- data/Rakefile +27 -0
- data/VERSION.yml +4 -0
- data/lib/mlist.rb +13 -0
- data/lib/mlist/email_server.rb +3 -0
- data/lib/mlist/email_server/base.rb +22 -0
- data/lib/mlist/email_server/email.rb +47 -0
- data/lib/mlist/email_server/fake.rb +16 -0
- data/lib/mlist/list.rb +49 -0
- data/lib/mlist/mail_list.rb +77 -0
- data/lib/mlist/manager/database.rb +34 -0
- data/lib/mlist/message.rb +107 -0
- data/lib/mlist/server.rb +36 -0
- data/lib/mlist/thread.rb +6 -0
- data/lib/mlist/util.rb +9 -0
- data/lib/mlist/util/header_sanitizer.rb +63 -0
- data/lib/mlist/util/quoting.rb +70 -0
- metadata +73 -0
data/README
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
= mlist
|
2
|
+
|
3
|
+
http://aiwilliams.github.com/mlist
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
An insane attempt to build a mail list server library that can be used
|
8
|
+
other applications very easily. The first target is Ruby applications
|
9
|
+
that can load MList models for direct integration. It will later have
|
10
|
+
a RESTful API so that non-Ruby applications can easily integrate. That
|
11
|
+
may depend heavily on community involvement...
|
12
|
+
|
13
|
+
== FEATURES/PROBLEMS:
|
14
|
+
|
15
|
+
There is a LOT to do: segmenting, spam filtering, HTML conversion, i18n,
|
16
|
+
backscatter - only the Mailman developers know what else. I have enough
|
17
|
+
experience to know that rewrites are NEVER as easy as they seem. I begin
|
18
|
+
this with fear and trepidation. Alas, I go boldly forward.
|
19
|
+
|
20
|
+
== SYNOPSIS:
|
21
|
+
|
22
|
+
Let's say you want your web application to have a mailing list feature.
|
23
|
+
Let's also say you care about the UI, and you don't want to learn all
|
24
|
+
about creating the correct HTML structures for a mailing list. You want to
|
25
|
+
have lots of power for searching the mail, and you have your own strategy
|
26
|
+
for managing the lists. You love Ruby.
|
27
|
+
|
28
|
+
== REQUIREMENTS:
|
29
|
+
|
30
|
+
You'll need some gems.
|
31
|
+
|
32
|
+
* activesupport
|
33
|
+
* activerecord
|
34
|
+
* tmail
|
35
|
+
|
36
|
+
== INSTALL:
|
37
|
+
|
38
|
+
* FIX (sudo gem install, anything else)
|
39
|
+
|
40
|
+
== LICENSE:
|
41
|
+
|
42
|
+
(The MIT License)
|
43
|
+
|
44
|
+
Copyright (c) 2008 Adam Williams (aiwilliams)
|
45
|
+
|
46
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
47
|
+
a copy of this software and associated documentation files (the
|
48
|
+
'Software'), to deal in the Software without restriction, including
|
49
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
50
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
51
|
+
permit persons to whom the Software is furnished to do so, subject to
|
52
|
+
the following conditions:
|
53
|
+
|
54
|
+
The above copyright notice and this permission notice shall be
|
55
|
+
included in all copies or substantial portions of the Software.
|
56
|
+
|
57
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
58
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
59
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
60
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
61
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
62
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
63
|
+
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,**/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/VERSION.yml
ADDED
data/lib/mlist.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
module MList
|
2
|
+
module EmailServer
|
3
|
+
class Base
|
4
|
+
def initialize
|
5
|
+
@receivers = []
|
6
|
+
end
|
7
|
+
|
8
|
+
def deliver(email)
|
9
|
+
raise 'Implement actual delivery mechanism in subclasses'
|
10
|
+
end
|
11
|
+
|
12
|
+
def receive(tmail)
|
13
|
+
email = EmailServer::Email.new(tmail)
|
14
|
+
@receivers.each { |r| r.receive(email) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def receiver(rx)
|
18
|
+
@receivers << rx
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module MList
|
2
|
+
module EmailServer
|
3
|
+
|
4
|
+
# The interface to an incoming email.
|
5
|
+
#
|
6
|
+
# My primary goal is to decouple the MList::EmailServer from the
|
7
|
+
# MList::Server, this class acting as the bridge.
|
8
|
+
#
|
9
|
+
class Email
|
10
|
+
|
11
|
+
# TODO Provide the email_server to the instances
|
12
|
+
def initialize(tmail)
|
13
|
+
@tmail = tmail
|
14
|
+
end
|
15
|
+
|
16
|
+
def from_address
|
17
|
+
@tmail.from.first
|
18
|
+
end
|
19
|
+
|
20
|
+
# Answers the usable destination addresses of the email.
|
21
|
+
#
|
22
|
+
# TODO: Provide intelligence to this that allows it to ignore addresses
|
23
|
+
# that are not for the domain of the email_server.
|
24
|
+
#
|
25
|
+
def list_addresses
|
26
|
+
bounce? ? @tmail.header_string('to').match(/\Amlist-(.*)\Z/)[1] : @tmail.to
|
27
|
+
end
|
28
|
+
|
29
|
+
# Answers true if this email is a bounce.
|
30
|
+
#
|
31
|
+
# TODO Delegate to the email_server's bounce detector.
|
32
|
+
#
|
33
|
+
def bounce?
|
34
|
+
@tmail.header_string('to') =~ /mlist-/
|
35
|
+
end
|
36
|
+
|
37
|
+
# Answers unique copies of the underlying TMail::Mail instance,
|
38
|
+
# providing assurance that the MList::Server and it's sub-systems don't
|
39
|
+
# stomp all over each other by getting a reference to a single
|
40
|
+
# TMail::Mail instance.
|
41
|
+
#
|
42
|
+
def tmail
|
43
|
+
TMail::Mail.parse(@tmail.to_s)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/mlist/list.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
module MList
|
2
|
+
|
3
|
+
# Represents the interface of the lists that a list manager must answer.
|
4
|
+
# This is distinct from the MList::MailList to allow for greater flexibility
|
5
|
+
# in processing email coming to a list - that is, whatever you include this
|
6
|
+
# into may re-define behavior appropriately.
|
7
|
+
#
|
8
|
+
module List
|
9
|
+
def bounce(email)
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
def host
|
14
|
+
address.match(/@(.*)\Z/)[1]
|
15
|
+
end
|
16
|
+
|
17
|
+
def list_headers
|
18
|
+
{
|
19
|
+
'list-id' => list_id,
|
20
|
+
'list-archive' => (archive_url rescue nil),
|
21
|
+
'list-subscribe' => (subscribe_url rescue nil),
|
22
|
+
'list-unsubscribe' => (unsubscribe_url rescue nil),
|
23
|
+
'list-owner' => (owner_url rescue nil),
|
24
|
+
'list-help' => (help_url rescue nil),
|
25
|
+
'list-post' => post_url
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def list_id
|
30
|
+
"#{label} <#{address}>"
|
31
|
+
end
|
32
|
+
|
33
|
+
def name
|
34
|
+
address.match(/\A(.*?)@/)[1]
|
35
|
+
end
|
36
|
+
|
37
|
+
def post_url
|
38
|
+
address
|
39
|
+
end
|
40
|
+
|
41
|
+
def recipients(message)
|
42
|
+
subscriptions.collect(&:address) - [message.from_address]
|
43
|
+
end
|
44
|
+
|
45
|
+
def subscriber?(address)
|
46
|
+
!subscriptions.detect {|s| s.address == address}.nil?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module MList
|
2
|
+
class MailList < ActiveRecord::Base
|
3
|
+
def self.find_or_create_by_list(list)
|
4
|
+
mail_list = find_or_create_by_identifier(list.list_id)
|
5
|
+
mail_list.manager_list = list
|
6
|
+
mail_list
|
7
|
+
end
|
8
|
+
|
9
|
+
has_many :messages, :dependent => :delete_all
|
10
|
+
has_many :threads, :dependent => :delete_all
|
11
|
+
|
12
|
+
attr_accessor :manager_list
|
13
|
+
delegate :address, :recipients, :subscriptions,
|
14
|
+
:to => :manager_list
|
15
|
+
|
16
|
+
def post(email_server, message)
|
17
|
+
return unless process?(message)
|
18
|
+
prepare_delivery(message)
|
19
|
+
deliver(message, email_server)
|
20
|
+
end
|
21
|
+
|
22
|
+
def been_there?(message)
|
23
|
+
message.header_string('x-beenthere') == address
|
24
|
+
end
|
25
|
+
|
26
|
+
# http://mail.python.org/pipermail/mailman-developers/2006-April/018718.html
|
27
|
+
def bounce_headers
|
28
|
+
{'sender' => "mlist-#{address}",
|
29
|
+
'errors-to' => "mlist-#{address}"}
|
30
|
+
end
|
31
|
+
|
32
|
+
def deliver(message, email_server)
|
33
|
+
transaction do
|
34
|
+
email_server.deliver(message.tmail)
|
35
|
+
thread = find_thread(message)
|
36
|
+
thread.messages << message
|
37
|
+
thread.save!
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def find_thread(message)
|
42
|
+
if message.reply?
|
43
|
+
threads.find(:first,
|
44
|
+
:joins => :messages,
|
45
|
+
:readonly => false,
|
46
|
+
:conditions => ['messages.identifier = ?', message.parent_identifier]
|
47
|
+
)
|
48
|
+
else
|
49
|
+
threads.build
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# http://www.jamesshuggins.com/h/web1/list-email-headers.htm
|
54
|
+
def list_headers
|
55
|
+
headers = manager_list.list_headers
|
56
|
+
headers['x-beenthere'] = address
|
57
|
+
headers.update(bounce_headers)
|
58
|
+
headers.delete_if {|k,v| v.nil?}
|
59
|
+
end
|
60
|
+
|
61
|
+
def prepare_delivery(message)
|
62
|
+
prepare_list_headers(message)
|
63
|
+
message.to = address
|
64
|
+
message.bcc = recipients(message)
|
65
|
+
end
|
66
|
+
|
67
|
+
def prepare_list_headers(message)
|
68
|
+
list_headers.each do |k,v|
|
69
|
+
message.write_header(k,v)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def process?(message)
|
74
|
+
!been_there?(message) && !recipients(message).blank?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module MList
|
2
|
+
module Manager
|
3
|
+
|
4
|
+
class Database
|
5
|
+
def create_list(address, attributes = {})
|
6
|
+
attributes = {
|
7
|
+
:address => address,
|
8
|
+
:label => address.match(/\A(.*?)@/)[1]
|
9
|
+
}.merge(attributes)
|
10
|
+
List.create!(attributes)
|
11
|
+
end
|
12
|
+
|
13
|
+
def lists(email)
|
14
|
+
lists = List.find_all_by_address(email.list_addresses)
|
15
|
+
email.list_addresses.map { |a| lists.detect {|l| l.address == a} }
|
16
|
+
end
|
17
|
+
|
18
|
+
class List < ActiveRecord::Base
|
19
|
+
include ::MList::List
|
20
|
+
|
21
|
+
has_many :subscriptions, :dependent => :delete_all
|
22
|
+
|
23
|
+
def subscribe(address)
|
24
|
+
subscriptions.find_or_create_by_address(address)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Subscription < ActiveRecord::Base
|
29
|
+
belongs_to :list
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module MList
|
2
|
+
|
3
|
+
# The persisted version of an email that is processed by MList::MailLists.
|
4
|
+
#
|
5
|
+
# The tmail object referenced by these are unique, though they may reference
|
6
|
+
# the 'same' originating email.
|
7
|
+
#
|
8
|
+
class Message < ActiveRecord::Base
|
9
|
+
belongs_to :mail_list
|
10
|
+
|
11
|
+
attr_writer :header_sanitizers
|
12
|
+
before_save :serialize_tmail
|
13
|
+
|
14
|
+
def charset
|
15
|
+
'utf-8'
|
16
|
+
end
|
17
|
+
|
18
|
+
def delete_header(name)
|
19
|
+
tmail[name] = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def from_address
|
23
|
+
tmail.from.first
|
24
|
+
end
|
25
|
+
|
26
|
+
def parent_identifier
|
27
|
+
if in_reply_to = header_string('in-reply-to')
|
28
|
+
identifier = in_reply_to
|
29
|
+
elsif references = read_header('references')
|
30
|
+
identifier = references.ids.first
|
31
|
+
else
|
32
|
+
parent_message = mail_list.messages.find(:first,
|
33
|
+
:conditions => ['messages.subject = ?', remove_regard(subject)],
|
34
|
+
:order => 'created_at asc'
|
35
|
+
)
|
36
|
+
identifier = parent_message.identifier if parent_message
|
37
|
+
end
|
38
|
+
remove_brackets(identifier) if identifier
|
39
|
+
end
|
40
|
+
|
41
|
+
def read_header(name)
|
42
|
+
tmail[name]
|
43
|
+
end
|
44
|
+
|
45
|
+
def reply?
|
46
|
+
!parent_identifier.nil?
|
47
|
+
end
|
48
|
+
|
49
|
+
def write_header(name, value)
|
50
|
+
tmail[name] = sanitize_header(name, value)
|
51
|
+
end
|
52
|
+
|
53
|
+
def tmail=(tmail)
|
54
|
+
write_attribute(:identifier, remove_brackets(tmail.header_string('message-id')))
|
55
|
+
write_attribute(:subject, tmail.subject)
|
56
|
+
@tmail = tmail
|
57
|
+
end
|
58
|
+
|
59
|
+
def tmail
|
60
|
+
@tmail ||= TMail::Mail.parse(email_text)
|
61
|
+
end
|
62
|
+
|
63
|
+
def to=(recipients)
|
64
|
+
tmail.to = sanitize_header('to', recipients)
|
65
|
+
end
|
66
|
+
|
67
|
+
def bcc=(recipients)
|
68
|
+
tmail.bcc = sanitize_header('bcc', recipients)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Provide delegation to *most* of the underlying TMail::Mail methods,
|
72
|
+
# excluding those overridden by this class and the [] and []= methods. We
|
73
|
+
# must maintain the ActiveRecord interface over that of the TMail::Mail
|
74
|
+
# interface.
|
75
|
+
#
|
76
|
+
def method_missing(symbol, *args, &block) # :nodoc:
|
77
|
+
if @tmail && @tmail.respond_to?(symbol) && !(symbol == :[] || symbol == :[]=)
|
78
|
+
@tmail.__send__(symbol, *args, &block)
|
79
|
+
else
|
80
|
+
super
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def sanitize_header(name, *values)
|
85
|
+
header_sanitizer(name).call(charset, *values)
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
def header_sanitizer(name)
|
90
|
+
@header_sanitizers ||= Util.default_header_sanitizers
|
91
|
+
@header_sanitizers[name]
|
92
|
+
end
|
93
|
+
|
94
|
+
def remove_brackets(string)
|
95
|
+
string =~ /\A<(.*?)>\Z/ ? $1 : string
|
96
|
+
end
|
97
|
+
|
98
|
+
def remove_regard(string)
|
99
|
+
stripped = string.strip
|
100
|
+
stripped =~ /\Are:\s+(.*?)\Z/i ? $1 : stripped
|
101
|
+
end
|
102
|
+
|
103
|
+
def serialize_tmail
|
104
|
+
write_attribute(:email_text, @tmail.to_s)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
data/lib/mlist/server.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
module MList
|
2
|
+
class Server
|
3
|
+
attr_reader :list_manager, :email_server
|
4
|
+
|
5
|
+
def initialize(config)
|
6
|
+
@list_manager = config[:list_manager]
|
7
|
+
@email_server = config[:email_server]
|
8
|
+
@email_server.receiver(self)
|
9
|
+
end
|
10
|
+
|
11
|
+
def receive(email)
|
12
|
+
lists = list_manager.lists(email)
|
13
|
+
if email.bounce?
|
14
|
+
process_bounce(lists.first, email)
|
15
|
+
else
|
16
|
+
process_post(lists, email)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
def process_bounce(list, email)
|
22
|
+
list.bounce(email)
|
23
|
+
end
|
24
|
+
|
25
|
+
def process_post(lists, email)
|
26
|
+
lists.each do |list|
|
27
|
+
if list.subscriber?(email.from_address)
|
28
|
+
mail_list = MailList.find_or_create_by_list(list)
|
29
|
+
mail_list.post(email_server, MList::Message.new(:mail_list => mail_list, :tmail => email.tmail))
|
30
|
+
else
|
31
|
+
list.non_subscriber_posted(email)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/mlist/thread.rb
ADDED
data/lib/mlist/util.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
module MList
|
2
|
+
module Util
|
3
|
+
|
4
|
+
class QuotingSanitizer
|
5
|
+
include Quoting
|
6
|
+
|
7
|
+
def initialize(method, bracket_urls)
|
8
|
+
@method, @bracket_urls = method, bracket_urls
|
9
|
+
end
|
10
|
+
|
11
|
+
def bracket_urls(values)
|
12
|
+
values.map do |value|
|
13
|
+
if value.include?('<') && value.include?('>')
|
14
|
+
value
|
15
|
+
else
|
16
|
+
"<#{value}>"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(charset, *values)
|
22
|
+
values = bracket_urls(values.flatten) if @bracket_urls
|
23
|
+
send(@method, charset, *values)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class HeaderSanitizerHash
|
28
|
+
def initialize
|
29
|
+
@hash = Hash.new
|
30
|
+
initialize_default_sanitizers
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize_default_sanitizers
|
34
|
+
self['to'] = quoter(:quote_any_address_if_necessary)
|
35
|
+
self['cc'] = quoter(:quote_any_address_if_necessary)
|
36
|
+
self['bcc'] = quoter(:quote_any_address_if_necessary)
|
37
|
+
self['from'] = quoter(:quote_any_address_if_necessary)
|
38
|
+
self['reply-to'] = quoter(:quote_any_address_if_necessary)
|
39
|
+
self['subject'] = quoter(:quote_any_if_necessary)
|
40
|
+
|
41
|
+
self['List-Help'] = quoter(:quote_address_if_necessary)
|
42
|
+
self['List-Subscribe'] = quoter(:quote_address_if_necessary)
|
43
|
+
self['List-Unsubscribe'] = quoter(:quote_address_if_necessary)
|
44
|
+
self['List-Post'] = quoter(:quote_address_if_necessary)
|
45
|
+
self['List-Owner'] = quoter(:quote_address_if_necessary)
|
46
|
+
self['List-Archive'] = quoter(:quote_address_if_necessary)
|
47
|
+
end
|
48
|
+
|
49
|
+
def [](key)
|
50
|
+
@hash[key.downcase] ||= lambda { |charset, value| value }
|
51
|
+
end
|
52
|
+
|
53
|
+
def []=(key, value)
|
54
|
+
@hash[key.downcase] = value
|
55
|
+
end
|
56
|
+
|
57
|
+
def quoter(method, bracket_urls = true)
|
58
|
+
QuotingSanitizer.new(method, bracket_urls)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module MList
|
2
|
+
module Util
|
3
|
+
|
4
|
+
# Copyright (c) 2004-2008 David Heinemeier Hansson
|
5
|
+
#
|
6
|
+
# Taken from ActionMailer. Modified to make charset first argument in all
|
7
|
+
# signatures, allowing for a consistent pattern of invocation.
|
8
|
+
#
|
9
|
+
module Quoting #:nodoc:
|
10
|
+
# Convert the given text into quoted printable format, with an instruction
|
11
|
+
# that the text be eventually interpreted in the given charset.
|
12
|
+
def quoted_printable(charset, text)
|
13
|
+
text = text.gsub( /[^a-z ]/i ) { quoted_printable_encode($&) }.
|
14
|
+
gsub( / /, "_" )
|
15
|
+
"=?#{charset}?Q?#{text}?="
|
16
|
+
end
|
17
|
+
|
18
|
+
# Convert the given character to quoted printable format, taking into
|
19
|
+
# account multi-byte characters (if executing with $KCODE="u", for instance)
|
20
|
+
def quoted_printable_encode(character)
|
21
|
+
result = ""
|
22
|
+
character.each_byte { |b| result << "=%02x" % b }
|
23
|
+
result
|
24
|
+
end
|
25
|
+
|
26
|
+
# A quick-and-dirty regexp for determining whether a string contains any
|
27
|
+
# characters that need escaping.
|
28
|
+
if !defined?(CHARS_NEEDING_QUOTING)
|
29
|
+
CHARS_NEEDING_QUOTING = /[\000-\011\013\014\016-\037\177-\377]/
|
30
|
+
end
|
31
|
+
|
32
|
+
# Quote the given text if it contains any "illegal" characters
|
33
|
+
def quote_if_necessary(charset, text)
|
34
|
+
text = text.dup.force_encoding(Encoding::ASCII_8BIT) if text.respond_to?(:force_encoding)
|
35
|
+
|
36
|
+
(text =~ CHARS_NEEDING_QUOTING) ?
|
37
|
+
quoted_printable(charset, text) :
|
38
|
+
text
|
39
|
+
end
|
40
|
+
|
41
|
+
# Quote any of the given strings if they contain any "illegal" characters
|
42
|
+
def quote_any_if_necessary(charset, *args)
|
43
|
+
args.map { |v| quote_if_necessary(charset, v) }
|
44
|
+
end
|
45
|
+
|
46
|
+
# Quote the given address if it needs to be. The address may be a
|
47
|
+
# regular email address, or it can be a phrase followed by an address in
|
48
|
+
# brackets. The phrase is the only part that will be quoted, and only if
|
49
|
+
# it needs to be. This allows extended characters to be used in the
|
50
|
+
# "to", "from", "cc", "bcc" and "reply-to" headers.
|
51
|
+
def quote_address_if_necessary(charset, address)
|
52
|
+
if Array === address
|
53
|
+
address.map { |a| quote_address_if_necessary(charset, a) }
|
54
|
+
elsif address =~ /^(\S.*)\s+(<.*>)$/
|
55
|
+
address = $2
|
56
|
+
phrase = quote_if_necessary(charset, $1.gsub(/^['"](.*)['"]$/, '\1'))
|
57
|
+
"\"#{phrase}\" #{address}"
|
58
|
+
else
|
59
|
+
address
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Quote any of the given addresses, if they need to be.
|
64
|
+
def quote_any_address_if_necessary(charset, *args)
|
65
|
+
args.map { |v| quote_address_if_necessary(charset, v) }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: aiwilliams-mlist
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Adam Williams
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-01-26 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: A Ruby mailing list library designed to be integrated into other applications.
|
17
|
+
email: adam@thewilliams.ws
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- Rakefile
|
26
|
+
- README
|
27
|
+
- VERSION.yml
|
28
|
+
- lib/mlist
|
29
|
+
- lib/mlist/email_server
|
30
|
+
- lib/mlist/email_server/base.rb
|
31
|
+
- lib/mlist/email_server/email.rb
|
32
|
+
- lib/mlist/email_server/fake.rb
|
33
|
+
- lib/mlist/email_server.rb
|
34
|
+
- lib/mlist/list.rb
|
35
|
+
- lib/mlist/mail_list.rb
|
36
|
+
- lib/mlist/manager
|
37
|
+
- lib/mlist/manager/database.rb
|
38
|
+
- lib/mlist/message.rb
|
39
|
+
- lib/mlist/server.rb
|
40
|
+
- lib/mlist/thread.rb
|
41
|
+
- lib/mlist/util
|
42
|
+
- lib/mlist/util/header_sanitizer.rb
|
43
|
+
- lib/mlist/util/quoting.rb
|
44
|
+
- lib/mlist/util.rb
|
45
|
+
- lib/mlist.rb
|
46
|
+
has_rdoc: false
|
47
|
+
homepage: http://github.com/aiwilliams/mlist
|
48
|
+
post_install_message:
|
49
|
+
rdoc_options: []
|
50
|
+
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: "0"
|
58
|
+
version:
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: "0"
|
64
|
+
version:
|
65
|
+
requirements: []
|
66
|
+
|
67
|
+
rubyforge_project:
|
68
|
+
rubygems_version: 1.2.0
|
69
|
+
signing_key:
|
70
|
+
specification_version: 2
|
71
|
+
summary: A Ruby mailing list library designed to be integrated into other applications.
|
72
|
+
test_files: []
|
73
|
+
|