aiwilliams-mlist 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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.
@@ -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
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 0
3
+ :patch: 0
4
+ :major: 0
@@ -0,0 +1,13 @@
1
+ require 'tmail'
2
+ require 'activerecord'
3
+
4
+ require 'mlist/util'
5
+ require 'mlist/message'
6
+ require 'mlist/list'
7
+ require 'mlist/mail_list'
8
+ require 'mlist/email_server'
9
+ require 'mlist/server'
10
+ require 'mlist/thread'
11
+
12
+ module MList
13
+ end
@@ -0,0 +1,3 @@
1
+ require 'mlist/email_server/email'
2
+ require 'mlist/email_server/base'
3
+ require 'mlist/email_server/fake'
@@ -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
@@ -0,0 +1,16 @@
1
+ module MList
2
+ module EmailServer
3
+ class Fake < Base
4
+ attr_reader :deliveries
5
+
6
+ def initialize
7
+ super
8
+ @deliveries = []
9
+ end
10
+
11
+ def deliver(tmail)
12
+ @deliveries << tmail
13
+ end
14
+ end
15
+ end
16
+ end
@@ -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
@@ -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
@@ -0,0 +1,6 @@
1
+ module MList
2
+ class Thread < ActiveRecord::Base
3
+ belongs_to :mail_list
4
+ has_many :messages, :dependent => :delete_all
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ require 'mlist/util/quoting'
2
+ require 'mlist/util/header_sanitizer'
3
+
4
+ module MList
5
+ module Util
6
+ mattr_accessor :default_header_sanitizers
7
+ self.default_header_sanitizers = HeaderSanitizerHash.new
8
+ end
9
+ end
@@ -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
+