aiwilliams-mlist 0.0.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 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
+