mailmanager 1.0.8
Sign up to get free protection for your applications and to get access to all the features.
- data/Changelog +8 -0
- data/LICENSE +24 -0
- data/README.rdoc +38 -0
- data/lib/mailmanager.rb +104 -0
- data/lib/mailmanager/lib.rb +194 -0
- data/lib/mailmanager/list.rb +129 -0
- data/lib/mailmanager/listproxy.py +73 -0
- data/lib/mailmanager/version.rb +3 -0
- data/spec/lib/mailmanager/lib_spec.rb +160 -0
- data/spec/lib/mailmanager/list_spec.rb +116 -0
- data/spec/lib/mailmanager_spec.rb +72 -0
- data/spec/spec_helper.rb +3 -0
- metadata +104 -0
data/Changelog
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
Copyright (c) 2011, Democratic National Committee
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
6
|
+
* Redistributions of source code must retain the above copyright
|
7
|
+
notice, this list of conditions and the following disclaimer.
|
8
|
+
* Redistributions in binary form must reproduce the above copyright
|
9
|
+
notice, this list of conditions and the following disclaimer in the
|
10
|
+
documentation and/or other materials provided with the distribution.
|
11
|
+
* Neither the name of the Democratic National Committee nor the
|
12
|
+
names of its contributors may be used to endorse or promote products
|
13
|
+
derived from this software without specific prior written permission.
|
14
|
+
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
16
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
17
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
18
|
+
DISCLAIMED. IN NO EVENT SHALL THE DEMOCRATIC NATIONAL COMMITTEE BE LIABLE FOR ANY
|
19
|
+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
20
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
21
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
22
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
23
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
24
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
= MailManager
|
2
|
+
|
3
|
+
MailManager is a Ruby wrapper for the GNU Mailman mailing list manager. It exposes
|
4
|
+
some administrative functions to Ruby. See the API docs for details.
|
5
|
+
It is licensed under the New BSD License (see the LICENSE file for details).
|
6
|
+
|
7
|
+
MailManager has been tested with Mailman 2.1.14. It has NOT been tested with any
|
8
|
+
release of Mailman 3 (in alpha as of 1/19/2011). It requires Python 2.6 or higher.
|
9
|
+
Note that while Mailman itself requires Python, it can work with older versions. So
|
10
|
+
check your Python version before using this.
|
11
|
+
|
12
|
+
== Installation
|
13
|
+
|
14
|
+
gem install mailmanager
|
15
|
+
|
16
|
+
== Basic Usage
|
17
|
+
|
18
|
+
mm = MailManager.init('/mailman/root')
|
19
|
+
all_lists = mm.lists
|
20
|
+
foo_list = mm.get_list('foo')
|
21
|
+
new_list = mm.create_list(:name => 'newlist', :admin_email => 'me@here.com', :admin_password => 'secret')
|
22
|
+
new_list.add_member('bar@baz.com')
|
23
|
+
new_list.members (returns ['foo@baz.com'])
|
24
|
+
|
25
|
+
== Contributing
|
26
|
+
|
27
|
+
- Fork on GitHub
|
28
|
+
- Make a new branch
|
29
|
+
- Push your changes to that branch (i.e. not master)
|
30
|
+
- Make a pull request
|
31
|
+
- Bask in your awesomeness
|
32
|
+
|
33
|
+
== Author
|
34
|
+
|
35
|
+
- Wes Morgan (cap10morgan on GitHub)
|
36
|
+
|
37
|
+
Copyright 2011 Democratic National Committee,
|
38
|
+
All Rights Reserved.
|
data/lib/mailmanager.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
require "singleton"
|
2
|
+
require "rubygems"
|
3
|
+
require "bundler/setup"
|
4
|
+
require "json"
|
5
|
+
require "open4"
|
6
|
+
|
7
|
+
require 'mailmanager/lib'
|
8
|
+
require 'mailmanager/list'
|
9
|
+
|
10
|
+
module MailManager
|
11
|
+
@root = nil
|
12
|
+
@python = '/usr/bin/env python'
|
13
|
+
@debug = ENV['MAILMANAGER_DEBUG'] =~ /^(?:1|true|y|yes|on)$/i ? true : false
|
14
|
+
|
15
|
+
def self.root=(root) #:nodoc:
|
16
|
+
@root = root
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.root #:nodoc:
|
20
|
+
@root
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.python=(python) #:nodoc:
|
24
|
+
@python = python
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.python #:nodoc:
|
28
|
+
@python
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.debug #:nodoc:
|
32
|
+
@debug
|
33
|
+
end
|
34
|
+
|
35
|
+
# Call this method to start using MailManager. Give it the full path to your
|
36
|
+
# Mailman installation. It will return an instance of MailManager::Base.
|
37
|
+
def self.init(root)
|
38
|
+
self.root = root
|
39
|
+
Base.instance
|
40
|
+
end
|
41
|
+
|
42
|
+
# The MailManager::Base class is the root class for working with a Mailman
|
43
|
+
# installation. You get an instance of it by calling
|
44
|
+
# MailManager.init('/mailman/root').
|
45
|
+
class Base
|
46
|
+
include Singleton
|
47
|
+
|
48
|
+
REQUIRED_BIN_FILES = ['list_lists', 'newlist', 'inject'] #:nodoc:
|
49
|
+
|
50
|
+
def initialize #:nodoc:
|
51
|
+
raise "Must set MailManager.root before calling #{self.class}.instance" if MailManager.root.nil?
|
52
|
+
raise "#{root} does not exist" unless Dir.exist?(root)
|
53
|
+
raise "#{root}/bin does not exist" unless Dir.exist?("#{root}/bin")
|
54
|
+
REQUIRED_BIN_FILES.each do |bin_file|
|
55
|
+
raise "#{root}/bin/#{bin_file} not found" unless File.exist?("#{root}/bin/#{bin_file}")
|
56
|
+
end
|
57
|
+
@lib = MailManager::Lib.new
|
58
|
+
end
|
59
|
+
|
60
|
+
# If you want to use a non-default python executable to run the Python
|
61
|
+
# portions of this gem, set its full path here. Since we require Python
|
62
|
+
# 2.6+ and some distros don't ship with that version, you can point this at
|
63
|
+
# a newer Python you have installed. Defaults to /usr/bin/env python.
|
64
|
+
def python=(python)
|
65
|
+
MailManager.python = python
|
66
|
+
end
|
67
|
+
|
68
|
+
def python #:nodoc:
|
69
|
+
MailManager.python
|
70
|
+
end
|
71
|
+
|
72
|
+
def root #:nodoc:
|
73
|
+
MailManager.root
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns an array of MailManager::List instances of the lists in your
|
77
|
+
# Mailman installation.
|
78
|
+
def lists
|
79
|
+
@lib.lists
|
80
|
+
end
|
81
|
+
|
82
|
+
# Only retrieves the list names, doesn't wrap them in MailManager::List
|
83
|
+
# instances.
|
84
|
+
def list_names
|
85
|
+
lists.map { |list| list.name }
|
86
|
+
end
|
87
|
+
|
88
|
+
# Create a new list. Returns an instance of MailManager::List. Params are:
|
89
|
+
# * :name => 'new_list_name'
|
90
|
+
# * :admin_email => 'admin@domain.com'
|
91
|
+
# * :admin_password => 'supersecret'
|
92
|
+
def create_list(params)
|
93
|
+
MailManager::List.create(params)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Get an existing list as a MailManager::List instance. Raises an exception if
|
97
|
+
# the list doesn't exist.
|
98
|
+
def get_list(list_name)
|
99
|
+
raise "#{list_name} list does not exist" unless list_names.include?(list_name.downcase)
|
100
|
+
MailManager::List.new(list_name)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
module MailManager
|
2
|
+
|
3
|
+
class MailmanExecuteError < StandardError #:nodoc:
|
4
|
+
end
|
5
|
+
|
6
|
+
class Lib #:nodoc:all
|
7
|
+
|
8
|
+
def mailmanager
|
9
|
+
MailManager::Base.instance
|
10
|
+
end
|
11
|
+
|
12
|
+
def lists
|
13
|
+
cmd = :list_lists
|
14
|
+
out = command(cmd)
|
15
|
+
parse_output(cmd, out)
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_list(params)
|
19
|
+
cmd = :newlist
|
20
|
+
out = command(cmd, params)
|
21
|
+
parse_output(cmd, out)
|
22
|
+
end
|
23
|
+
|
24
|
+
def list_address(list)
|
25
|
+
cmd = :withlist
|
26
|
+
out = command(cmd, :name => list.name, :wlcmd => :getListAddress)
|
27
|
+
parse_json_output(out)
|
28
|
+
end
|
29
|
+
|
30
|
+
def regular_members(list)
|
31
|
+
cmd = :withlist
|
32
|
+
out = command(cmd, :name => list.name, :wlcmd => :getRegularMemberKeys)
|
33
|
+
parse_json_output(out)
|
34
|
+
end
|
35
|
+
|
36
|
+
def digest_members(list)
|
37
|
+
cmd = :withlist
|
38
|
+
out = command(cmd, :name => list.name, :wlcmd => :getDigestMemberKeys)
|
39
|
+
parse_json_output(out)
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_member(list, member)
|
43
|
+
cmd = :withlist
|
44
|
+
out = command(cmd, :name => list.name, :wlcmd => :AddMember, :arg => member)
|
45
|
+
parse_json_output(out)
|
46
|
+
end
|
47
|
+
|
48
|
+
def approved_add_member(list, member)
|
49
|
+
cmd = :withlist
|
50
|
+
out = command(cmd, :name => list.name, :wlcmd => :ApprovedAddMember,
|
51
|
+
:arg => member)
|
52
|
+
parse_json_output(out)
|
53
|
+
end
|
54
|
+
|
55
|
+
def delete_member(list, email)
|
56
|
+
cmd = :withlist
|
57
|
+
out = command(cmd, :name => list.name, :wlcmd => :DeleteMember,
|
58
|
+
:arg => email)
|
59
|
+
parse_json_output(out)
|
60
|
+
end
|
61
|
+
|
62
|
+
def approved_delete_member(list, email)
|
63
|
+
cmd = :withlist
|
64
|
+
out = command(cmd, :name => list.name, :wlcmd => :ApprovedDeleteMember,
|
65
|
+
:arg => email)
|
66
|
+
parse_json_output(out)
|
67
|
+
end
|
68
|
+
|
69
|
+
def moderators(list)
|
70
|
+
cmd = :withlist
|
71
|
+
out = command(cmd, :name => list.name, :wlcmd => :moderator)
|
72
|
+
parse_json_output(out)
|
73
|
+
end
|
74
|
+
|
75
|
+
def add_moderator(list, email)
|
76
|
+
if moderators(list)['return'].include?(email)
|
77
|
+
return {'result' => 'already_a_moderator'}
|
78
|
+
end
|
79
|
+
cmd = :withlist
|
80
|
+
out = command(cmd, :name => list.name, :wlcmd => 'moderator.append',
|
81
|
+
:arg => email)
|
82
|
+
parse_json_output(out)
|
83
|
+
end
|
84
|
+
|
85
|
+
def delete_moderator(list, email)
|
86
|
+
raise "#{email} is not a moderator" unless moderators(list)['return'].include?(email)
|
87
|
+
cmd = :withlist
|
88
|
+
out = command(cmd, :name => list.name, :wlcmd => 'moderator.remove',
|
89
|
+
:arg => email)
|
90
|
+
parse_json_output(out)
|
91
|
+
end
|
92
|
+
|
93
|
+
def inject(list, message, queue=nil)
|
94
|
+
cmd = :inject
|
95
|
+
params = {:listname => list.name, :stdin => message}
|
96
|
+
params[:queue] = queue unless queue.nil?
|
97
|
+
command(cmd, params)
|
98
|
+
end
|
99
|
+
|
100
|
+
def command(cmd, opts = {})
|
101
|
+
mailman_cmd = "#{mailmanager.root}/bin/#{cmd.to_s} "
|
102
|
+
# delete opts as we handle them explicitly
|
103
|
+
stdin = nil
|
104
|
+
stdin = opts.delete(:stdin) if opts.has_key?(:stdin)
|
105
|
+
case cmd
|
106
|
+
when :newlist
|
107
|
+
mailman_cmd += "-q "
|
108
|
+
raise ArgumentError, "Missing :name param" if opts[:name].nil?
|
109
|
+
raise ArgumentError, "Missing :admin_email param" if opts[:admin_email].nil?
|
110
|
+
raise ArgumentError, "Missing :admin_password param" if opts[:admin_password].nil?
|
111
|
+
mailman_cmd_suffix = [:name, :admin_email, :admin_password].map { |key|
|
112
|
+
escape(opts.delete(key))
|
113
|
+
}.join(' ')
|
114
|
+
mailman_cmd += "#{mailman_cmd_suffix} "
|
115
|
+
when :withlist
|
116
|
+
raise ArgumentError, "Missing :name param" if opts[:name].nil?
|
117
|
+
proxy_path = File.dirname(__FILE__)
|
118
|
+
mailman_cmd = "PYTHONPATH=#{proxy_path} #{MailManager.python} #{mailman_cmd}"
|
119
|
+
mailman_cmd += "-q -r listproxy.command #{escape(opts.delete(:name))} " +
|
120
|
+
"#{opts.delete(:wlcmd)} "
|
121
|
+
if !opts[:arg].nil? && opts[:arg].length > 0
|
122
|
+
mailman_cmd += "#{escape(opts.delete(:arg))} "
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# assume any leftover opts are POSIX-style args
|
127
|
+
mailman_cmd += opts.keys.map { |k| "--#{k}=#{escape(opts[k])}" }.join(' ')
|
128
|
+
mailman_cmd += ' ' if mailman_cmd[-1,1] != ' '
|
129
|
+
mailman_cmd += "2>&1"
|
130
|
+
if MailManager.debug
|
131
|
+
puts "Running mailman command: #{mailman_cmd}"
|
132
|
+
puts " with stdin: #{stdin}" unless stdin.nil?
|
133
|
+
end
|
134
|
+
out, process = run_command(mailman_cmd, stdin)
|
135
|
+
|
136
|
+
if process.exitstatus > 0
|
137
|
+
raise MailManager::MailmanExecuteError.new(mailman_cmd + ':' + out.to_s)
|
138
|
+
end
|
139
|
+
out
|
140
|
+
end
|
141
|
+
|
142
|
+
def run_command(mailman_cmd, stdindata=nil)
|
143
|
+
output = nil
|
144
|
+
process = Open4::popen4(mailman_cmd) do |pid, stdin, stdout, stderr|
|
145
|
+
if !stdindata.nil?
|
146
|
+
stdin.puts(stdindata)
|
147
|
+
stdin.close
|
148
|
+
end
|
149
|
+
output = stdout.read
|
150
|
+
end
|
151
|
+
[output, process]
|
152
|
+
end
|
153
|
+
|
154
|
+
def escape(s)
|
155
|
+
# no idea what this does, stole it from the ruby-git gem
|
156
|
+
escaped = s.to_s.gsub('\'', '\'\\\'\'')
|
157
|
+
%Q{"#{escaped}"}
|
158
|
+
end
|
159
|
+
|
160
|
+
def parse_output(mailman_cmd, output)
|
161
|
+
case mailman_cmd
|
162
|
+
when :newlist
|
163
|
+
list_name = nil
|
164
|
+
output.split("\n").each do |line|
|
165
|
+
if match = /^##\s+(.+?)mailing\s+list\s*$/.match(line)
|
166
|
+
list_name = match[1]
|
167
|
+
end
|
168
|
+
end
|
169
|
+
raise "Error getting name of newly created list" if list_name.nil?
|
170
|
+
return_obj = MailManager::List.new(list_name)
|
171
|
+
when :list_lists
|
172
|
+
lists = []
|
173
|
+
puts "Output from Mailman:\n#{output}" if MailManager.debug
|
174
|
+
output.split("\n").each do |line|
|
175
|
+
next if line =~ /^\d+ matching mailing lists found:$/
|
176
|
+
/^\s*(.+?)\s+-\s+(.+)$/.match(line) do |m|
|
177
|
+
puts "Found list #{m[1]}" if MailManager.debug
|
178
|
+
lists << MailManager::List.new(m[1].downcase)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
return_obj = lists
|
182
|
+
end
|
183
|
+
return_obj
|
184
|
+
end
|
185
|
+
|
186
|
+
def parse_json_output(json)
|
187
|
+
result = JSON.parse(json)
|
188
|
+
if result.is_a?(Hash) && !result['error'].nil?
|
189
|
+
raise MailmanExecuteError, result['error']
|
190
|
+
end
|
191
|
+
result
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module MailManager
|
2
|
+
|
3
|
+
# The List class represents mailing lists in Mailman.
|
4
|
+
# Typically you get them by doing one of these things:
|
5
|
+
# mm = MailManager.init('/mailman/root')
|
6
|
+
# mylist = mm.get_list('list_name')
|
7
|
+
# OR
|
8
|
+
# mylist = mm.create_list(:name => 'list_name', :admin_email =>
|
9
|
+
# 'foo@bar.com', :admin_password => 'supersecret')
|
10
|
+
#
|
11
|
+
|
12
|
+
class List
|
13
|
+
# The name of the list
|
14
|
+
attr_reader :name
|
15
|
+
|
16
|
+
# This doesn't do any checking to see whether or not the requested list
|
17
|
+
# exists or not. Better to use MailManager::Base#get_list instead.
|
18
|
+
def initialize(name)
|
19
|
+
@name = name
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s #:nodoc:
|
23
|
+
@name
|
24
|
+
end
|
25
|
+
|
26
|
+
def lib #:nodoc:
|
27
|
+
self.class.lib
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.lib #:nodoc:
|
31
|
+
MailManager::Lib.new
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.create(params) #:nodoc:
|
35
|
+
lib.create_list(params)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns the list's email address
|
39
|
+
def address
|
40
|
+
result = lib.list_address(self)
|
41
|
+
result['return']
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns all list members (regular & digest) as an array
|
45
|
+
def members
|
46
|
+
regular_members + digest_members
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns just the regular list members (no digest members) as an array
|
50
|
+
def regular_members
|
51
|
+
result = lib.regular_members(self)
|
52
|
+
result['return']
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns just the digest list members (no regular members) as an array
|
56
|
+
def digest_members
|
57
|
+
result = lib.digest_members(self)
|
58
|
+
result['return']
|
59
|
+
end
|
60
|
+
|
61
|
+
# Adds a new list member, subject to the list's subscription rules
|
62
|
+
def add_member(email, name='')
|
63
|
+
add_member_using(:add_member, email, name)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Adds a new list member, bypassing the list's subscription rules
|
67
|
+
def approved_add_member(email, name='')
|
68
|
+
add_member_using(:approved_add_member, email, name)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Deletes a list member, subject to the list's unsubscription rules
|
72
|
+
def delete_member(email)
|
73
|
+
delete_member_using(:delete_member, email)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Deletes a list member, bypassing the list's unsubscription rules
|
77
|
+
def approved_delete_member(email, name='')
|
78
|
+
delete_member_using(:approved_delete_member, email)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns the list of moderators as an array of email addresses
|
82
|
+
def moderators
|
83
|
+
result = lib.moderators(self)
|
84
|
+
result['return']
|
85
|
+
end
|
86
|
+
|
87
|
+
# Adds a new moderator to the list
|
88
|
+
def add_moderator(email)
|
89
|
+
result = lib.add_moderator(self, email)
|
90
|
+
result['result'].to_sym
|
91
|
+
end
|
92
|
+
|
93
|
+
# Deletes a moderator from the list. Will raise an exception if the
|
94
|
+
# moderator doesn't exist yet.
|
95
|
+
def delete_moderator(email)
|
96
|
+
result = lib.delete_moderator(self, email)
|
97
|
+
result['result'].to_sym
|
98
|
+
end
|
99
|
+
|
100
|
+
# Injects a message into the list.
|
101
|
+
def inject(from, subject, message)
|
102
|
+
inject_message =<<EOF
|
103
|
+
From: #{from}
|
104
|
+
To: #{address}
|
105
|
+
Subject: #{subject}
|
106
|
+
|
107
|
+
#{message}
|
108
|
+
EOF
|
109
|
+
lib.inject(self, inject_message)
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def add_member_using(method, email, name)
|
115
|
+
if name.length > 0
|
116
|
+
member = "#{name} <#{email}>"
|
117
|
+
else
|
118
|
+
member = email
|
119
|
+
end
|
120
|
+
result = lib.send(method, self, member)
|
121
|
+
result['result'].to_sym
|
122
|
+
end
|
123
|
+
|
124
|
+
def delete_member_using(method, email)
|
125
|
+
result = lib.send(method, self, email)
|
126
|
+
result['result'].to_sym
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
import json
|
2
|
+
from email.Utils import parseaddr
|
3
|
+
from collections import Callable
|
4
|
+
from Mailman import MailList
|
5
|
+
from Mailman import Errors
|
6
|
+
|
7
|
+
class MailingListEncoder(json.JSONEncoder):
|
8
|
+
def default(self, obj):
|
9
|
+
if isinstance(obj, MailList.MailList):
|
10
|
+
return {'name': obj.internal_name()}
|
11
|
+
return json.JSONEncoder.default(self, obj)
|
12
|
+
|
13
|
+
def dumplist(mlist):
|
14
|
+
print json.dumps(mlist, True, cls=MailingListEncoder)
|
15
|
+
|
16
|
+
class UserDesc: pass
|
17
|
+
def userdesc_for(member):
|
18
|
+
userdesc = UserDesc()
|
19
|
+
userdesc.fullname, userdesc.address = parseaddr(member)
|
20
|
+
return userdesc
|
21
|
+
|
22
|
+
def unwindattrs(obj, attrs, *args):
|
23
|
+
if not attrs.count('.'):
|
24
|
+
attr = getattr(obj, attrs)
|
25
|
+
if isinstance(attr, Callable):
|
26
|
+
return attr(*args)
|
27
|
+
else:
|
28
|
+
return attr
|
29
|
+
else:
|
30
|
+
attr, nextattrs = attrs.split('.', 1)
|
31
|
+
nextobj = getattr(obj, attr)
|
32
|
+
return unwindattrs(nextobj, nextattrs, *args)
|
33
|
+
|
34
|
+
needs_userdesc = dict(AddMember=True, ApprovedAddMember=True)
|
35
|
+
needs_save = dict(AddMember=True, ApprovedAddMember=True,
|
36
|
+
DeleteMember=True, ApprovedDeleteMember=True,
|
37
|
+
moderator_append=True, moderator_remove=True)
|
38
|
+
|
39
|
+
def command(mlist, cmd, *args):
|
40
|
+
result = {}
|
41
|
+
try:
|
42
|
+
if needs_save.get(cmd.replace('.','_'), False):
|
43
|
+
mlist.Lock()
|
44
|
+
if needs_userdesc.get(cmd, False):
|
45
|
+
result['return'] = unwindattrs(mlist, cmd, userdesc_for(args[0]))
|
46
|
+
else:
|
47
|
+
result['return'] = unwindattrs(mlist, cmd, *args)
|
48
|
+
if needs_save.get(cmd.replace('.','_'), False):
|
49
|
+
mlist.Save()
|
50
|
+
except TypeError as err:
|
51
|
+
error_msg = '%s' % err
|
52
|
+
print json.dumps({'error': error_msg})
|
53
|
+
except AttributeError as err:
|
54
|
+
error_msg = 'AttributeError: %s' % err
|
55
|
+
print json.dumps({'error': error_msg})
|
56
|
+
except Errors.MMSubscribeNeedsConfirmation as err:
|
57
|
+
print json.dumps({'result': 'pending_confirmation'})
|
58
|
+
except Errors.MMAlreadyAMember as err:
|
59
|
+
print json.dumps({'result': 'already_a_member'})
|
60
|
+
except Errors.MMNeedApproval as err:
|
61
|
+
print json.dumps({'result': 'needs_approval'})
|
62
|
+
except Exception as err:
|
63
|
+
error_msg = '%s: %s' % (type(err), err)
|
64
|
+
print json.dumps({'error': error_msg})
|
65
|
+
else:
|
66
|
+
result['result'] = 'success'
|
67
|
+
print json.dumps(result)
|
68
|
+
|
69
|
+
#def loadlist(mlist, jsonlist):
|
70
|
+
#newlist = json.loads(jsonlist)
|
71
|
+
#for attr in newlist:
|
72
|
+
#print "Setting %s to %s" % (attr, newlist[attr])
|
73
|
+
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MailManager::Lib do
|
4
|
+
let(:mailmanager) { mock(MailManager) }
|
5
|
+
let(:subject) { MailManager::Lib.new }
|
6
|
+
let(:fake_root) { '/foo/bar' }
|
7
|
+
let(:process) { mock(Process::Status) }
|
8
|
+
|
9
|
+
before :each do
|
10
|
+
subject.stub(:mailmanager).and_return(mailmanager)
|
11
|
+
mailmanager.stub(:root).and_return(fake_root)
|
12
|
+
process.stub(:exitstatus).and_return(0)
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "#lists" do
|
16
|
+
it "should return all existing lists" do
|
17
|
+
list_result = <<EOF
|
18
|
+
3 matching mailing lists found:
|
19
|
+
Foo - [no description available]
|
20
|
+
BarBar - Dummy list
|
21
|
+
Mailman - Mailman site list
|
22
|
+
EOF
|
23
|
+
subject.stub(:run_command).with("#{fake_root}/bin/list_lists 2>&1", nil).
|
24
|
+
and_return([list_result,process])
|
25
|
+
subject.lists.should have(3).lists
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#create_list" do
|
30
|
+
it "should raise an argument error if list name is missing" do
|
31
|
+
lambda {
|
32
|
+
subject.create_list(:admin_email => 'foo@bar.baz', :admin_password => 'qux')
|
33
|
+
}.should raise_error(ArgumentError)
|
34
|
+
end
|
35
|
+
it "should raise an argument error if list admin email is missing" do
|
36
|
+
lambda {
|
37
|
+
subject.create_list(:name => 'foo', :admin_password => 'qux')
|
38
|
+
}.should raise_error(ArgumentError)
|
39
|
+
end
|
40
|
+
it "should raise an argument error if admin password is missing" do
|
41
|
+
lambda {
|
42
|
+
subject.create_list(:name => 'foo', :admin_email => 'foo@bar.baz')
|
43
|
+
}.should raise_error(ArgumentError)
|
44
|
+
end
|
45
|
+
|
46
|
+
context "with valid list params" do
|
47
|
+
let(:new_aliases) {
|
48
|
+
['foo: "|/foo/bar/mail/mailman post foo"',
|
49
|
+
'foo-admin: "|/foo/bar/mail/mailman admin foo"',
|
50
|
+
'foo-bounces: "|/foo/bar/mail/mailman bounces foo"',
|
51
|
+
'foo-confirm: "|/foo/bar/mail/mailman confirm foo"',
|
52
|
+
'foo-join: "|/foo/bar/mail/mailman join foo"',
|
53
|
+
'foo-leave: "|/foo/bar/mail/mailman leave foo"',
|
54
|
+
'foo-owner: "|/foo/bar/mail/mailman owner foo"',
|
55
|
+
'foo-request: "|/foo/bar/mail/mailman request foo"',
|
56
|
+
'foo-subscribe: "|/foo/bar/mail/mailman subscribe foo"',
|
57
|
+
'foo-unsubscribe: "|/foo/bar/mail/mailman unsubscribe foo"']
|
58
|
+
}
|
59
|
+
let(:new_list_return) {
|
60
|
+
prefix =<<EOF
|
61
|
+
To finish creating your mailing list, you must edit your /etc/aliases (or
|
62
|
+
equivalent) file by adding the following lines, and possibly running the
|
63
|
+
`newaliases' program:
|
64
|
+
|
65
|
+
## foo mailing list
|
66
|
+
EOF
|
67
|
+
prefix+new_aliases.join("\n")
|
68
|
+
}
|
69
|
+
let(:fake_aliases_file) { mock(File) }
|
70
|
+
|
71
|
+
before :each do
|
72
|
+
File.stub(:open).with('/etc/aliases', 'a').and_return(fake_aliases_file)
|
73
|
+
subject.stub(:run_newaliases_command)
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should create the list" do
|
77
|
+
subject.should_receive(:run_command).
|
78
|
+
with("#{fake_root}/bin/newlist -q \"foo\" \"foo@bar.baz\" \"qux\" 2>&1", nil).
|
79
|
+
and_return([new_list_return,process])
|
80
|
+
subject.create_list(:name => 'foo', :admin_email => 'foo@bar.baz',
|
81
|
+
:admin_password => 'qux')
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context "with populated list" do
|
87
|
+
let(:list) { list = mock(MailManager::List)
|
88
|
+
list.stub(:name).and_return('foo')
|
89
|
+
list }
|
90
|
+
|
91
|
+
let(:regular_members) { ['me@here.com', 'you@there.org'] }
|
92
|
+
let(:digest_members) { ['them@that.net'] }
|
93
|
+
|
94
|
+
let(:cmd) { "PYTHONPATH=#{File.expand_path('lib/mailmanager')} " +
|
95
|
+
"#{fake_root}/bin/withlist -q -r listproxy.command \"foo\" " }
|
96
|
+
|
97
|
+
describe "#regular_members" do
|
98
|
+
it "should ask Mailman for the regular list members" do
|
99
|
+
subject.should_receive(:run_command).
|
100
|
+
with(cmd+"getRegularMemberKeys 2>&1", nil).
|
101
|
+
and_return([JSON.generate(regular_members),process])
|
102
|
+
subject.regular_members(list).should == regular_members
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe "#digest_members" do
|
107
|
+
it "should ask Mailman for the digest list members" do
|
108
|
+
subject.should_receive(:run_command).
|
109
|
+
with(cmd+"getDigestMemberKeys 2>&1", nil).
|
110
|
+
and_return([JSON.generate(digest_members),process])
|
111
|
+
subject.digest_members(list).should == digest_members
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe "#add_member" do
|
116
|
+
it "should ask Mailman to add the member to the list" do
|
117
|
+
new_member = 'newb@dnc.org'
|
118
|
+
result = {"result" => "pending_confirmation"}
|
119
|
+
subject.should_receive(:run_command).
|
120
|
+
with(cmd+"AddMember \"#{new_member}\" 2>&1", nil).
|
121
|
+
and_return([JSON.generate(result),process])
|
122
|
+
subject.add_member(list, new_member).should == result
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe "#approved_add_member" do
|
127
|
+
it "should ask Mailman to add the member to the list" do
|
128
|
+
new_member = 'newb@dnc.org'
|
129
|
+
result = {"result" => "success"}
|
130
|
+
subject.should_receive(:run_command).
|
131
|
+
with(cmd+"ApprovedAddMember \"#{new_member}\" 2>&1", nil).
|
132
|
+
and_return([JSON.generate(result),process])
|
133
|
+
subject.approved_add_member(list, new_member).should == result
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
describe "#delete_member" do
|
138
|
+
it "should ask Mailman to delete the member from the list" do
|
139
|
+
former_member = 'oldie@ofa.org'
|
140
|
+
result = {"result" => "success"}
|
141
|
+
subject.should_receive(:run_command).
|
142
|
+
with(cmd+"DeleteMember \"#{former_member}\" 2>&1", nil).
|
143
|
+
and_return([JSON.generate(result),process])
|
144
|
+
subject.delete_member(list, former_member).should == result
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
describe "#approved_delete_member" do
|
149
|
+
it "should ask Mailman to delete the member from the list" do
|
150
|
+
former_member = 'oldie@ofa.org'
|
151
|
+
result = {"result" => "success"}
|
152
|
+
subject.should_receive(:run_command).
|
153
|
+
with(cmd+"ApprovedDeleteMember \"#{former_member}\" 2>&1", nil).
|
154
|
+
and_return([JSON.generate(result),process])
|
155
|
+
subject.approved_delete_member(list, former_member).should == result
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MailManager::List do
|
4
|
+
let(:lib) { mock(MailManager::Lib) }
|
5
|
+
let(:subject) { MailManager::List.new('foo') }
|
6
|
+
|
7
|
+
before :each do
|
8
|
+
MailManager::List.stub(:lib).and_return(lib)
|
9
|
+
end
|
10
|
+
|
11
|
+
describe ".create" do
|
12
|
+
it "should require the params arg" do
|
13
|
+
lambda {
|
14
|
+
MailManager::List.create
|
15
|
+
}.should raise_error(ArgumentError)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should return the new list" do
|
19
|
+
params = {:name => 'foo', :admin_email => 'foo@bar.baz', :admin_password => 'qux'}
|
20
|
+
lib.stub(:create_list).with(params).and_return(subject)
|
21
|
+
new_list = MailManager::List.create(params)
|
22
|
+
new_list.should_not be_nil
|
23
|
+
new_list.name.should == 'foo'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "#initialize" do
|
28
|
+
it "should take a name parameter" do
|
29
|
+
MailManager::List.new('foo').name.should == 'foo'
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should raise an error if the name arg is missing" do
|
33
|
+
lambda {
|
34
|
+
MailManager::List.new
|
35
|
+
}.should raise_error(ArgumentError)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "#to_s" do
|
40
|
+
it "should return its name" do
|
41
|
+
subject.to_s.should == "foo"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context "with list members" do
|
46
|
+
let(:regular_members) { ['me@here.com', 'you@there.org'] }
|
47
|
+
let(:digest_members) { ['them@that.net'] }
|
48
|
+
let(:all_members) { regular_members + digest_members }
|
49
|
+
|
50
|
+
describe "#regular_members" do
|
51
|
+
it "should return only regular members" do
|
52
|
+
lib.stub(:regular_members).with(subject).and_return({'return' => regular_members})
|
53
|
+
subject.regular_members.should == regular_members
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "#digest_members" do
|
58
|
+
it "should return only digest members" do
|
59
|
+
lib.stub(:digest_members).with(subject).and_return({'return' => digest_members})
|
60
|
+
subject.digest_members.should == digest_members
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "#members" do
|
65
|
+
it "should return the list of all members" do
|
66
|
+
lib.stub(:regular_members).with(subject).and_return({'return' => regular_members})
|
67
|
+
lib.stub(:digest_members).with(subject).and_return({'return' => digest_members})
|
68
|
+
subject.members.should == all_members
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "#add_member" do
|
74
|
+
it "should tell lib to add the member" do
|
75
|
+
lib.should_receive(:add_member).with(subject, 'foo@bar.baz').
|
76
|
+
and_return({'result' => 'pending_confirmation'})
|
77
|
+
subject.add_member('foo@bar.baz').should == :pending_confirmation
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should accept an optional name argument" do
|
81
|
+
lib.should_receive(:add_member).with(subject, 'Foo Bar <foo@bar.baz>').
|
82
|
+
and_return({'result' => 'pending_confirmation'})
|
83
|
+
subject.add_member('foo@bar.baz', 'Foo Bar').should == :pending_confirmation
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe "#approved_add_member" do
|
88
|
+
it "should tell lib to add the member" do
|
89
|
+
lib.should_receive(:approved_add_member).with(subject, 'foo@bar.baz').
|
90
|
+
and_return({'result' => 'success'})
|
91
|
+
subject.approved_add_member('foo@bar.baz').should == :success
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should accept an optional name argument" do
|
95
|
+
lib.should_receive(:approved_add_member).with(subject, 'Foo Bar <foo@bar.baz>').
|
96
|
+
and_return({'result' => 'success'})
|
97
|
+
subject.approved_add_member('foo@bar.baz', 'Foo Bar').should == :success
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
describe "#delete_member" do
|
102
|
+
it "should tell lib to delete the member" do
|
103
|
+
lib.should_receive(:delete_member).with(subject, 'foo@bar.baz').
|
104
|
+
and_return({'result' => 'success'})
|
105
|
+
subject.delete_member('foo@bar.baz').should == :success
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe "#approved_delete_member" do
|
110
|
+
it "should tell lib to delete the member" do
|
111
|
+
lib.should_receive(:approved_delete_member).with(subject, 'foo@bar.baz').
|
112
|
+
and_return({'result' => 'success'})
|
113
|
+
subject.approved_delete_member('foo@bar.baz').should == :success
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "MailManager" do
|
4
|
+
|
5
|
+
describe "Base" do
|
6
|
+
describe ".instance" do
|
7
|
+
it "should require setting the Mailman root directory first" do
|
8
|
+
lambda {
|
9
|
+
MailManager::Base.instance
|
10
|
+
}.should raise_error
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should require that the Mailman directory exist" do
|
14
|
+
lambda {
|
15
|
+
MailManager.root = '/foo/bar'
|
16
|
+
MailManager::Base.instance
|
17
|
+
}.should raise_error
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should require that the Mailman dir have a bin subdir" do
|
21
|
+
Dir.stub(:exist?).with('/foo/bar').and_return(true)
|
22
|
+
Dir.stub(:exist?).with('/foo/bar/bin').and_return(false)
|
23
|
+
lambda {
|
24
|
+
MailManager.root = '/foo/bar'
|
25
|
+
MailManager::Base.instance
|
26
|
+
}.should raise_error
|
27
|
+
end
|
28
|
+
|
29
|
+
context "with a valid Mailman dir" do
|
30
|
+
let(:mailman_path) { '/usr/local/mailman' }
|
31
|
+
let(:bin_files) { ['list_lists', 'newlist', 'inject'] }
|
32
|
+
|
33
|
+
before :each do
|
34
|
+
Dir.stub(:exist?).with(mailman_path).and_return(true)
|
35
|
+
Dir.stub(:exist?).with("#{mailman_path}/bin").and_return(true)
|
36
|
+
bin_files.each do |bf|
|
37
|
+
File.stub(:exist?).with("#{mailman_path}/bin/#{bf}").and_return(true)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should raise an error if one of the bin files is missing" do
|
42
|
+
File.stub(:exist?).with("#{mailman_path}/bin/inject").and_return(false)
|
43
|
+
lambda {
|
44
|
+
MailManager.root = mailman_path
|
45
|
+
MailManager::Base.instance
|
46
|
+
}.should raise_error
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should succeed if the all the bin files are present" do
|
50
|
+
MailManager.root = mailman_path
|
51
|
+
MailManager::Base.instance.should_not be_nil
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "#lists" do
|
55
|
+
it "should return an array of existing mailing lists" do
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "List" do
|
64
|
+
describe ".initialize" do
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
describe ".create" do
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mailmanager
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 1
|
7
|
+
- 0
|
8
|
+
- 8
|
9
|
+
version: 1.0.8
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Wes Morgan
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2011-01-19 00:00:00 -05:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: json
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 1
|
30
|
+
- 4
|
31
|
+
- 6
|
32
|
+
version: 1.4.6
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: open4
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
segments:
|
44
|
+
- 1
|
45
|
+
- 0
|
46
|
+
- 1
|
47
|
+
version: 1.0.1
|
48
|
+
type: :runtime
|
49
|
+
version_requirements: *id002
|
50
|
+
description: Ruby wrapper library for GNU Mailman's admin functions
|
51
|
+
email: MorganW@dnc.org
|
52
|
+
executables: []
|
53
|
+
|
54
|
+
extensions: []
|
55
|
+
|
56
|
+
extra_rdoc_files: []
|
57
|
+
|
58
|
+
files:
|
59
|
+
- lib/mailmanager/lib.rb
|
60
|
+
- lib/mailmanager/list.rb
|
61
|
+
- lib/mailmanager/version.rb
|
62
|
+
- lib/mailmanager.rb
|
63
|
+
- lib/mailmanager/listproxy.py
|
64
|
+
- spec/lib/mailmanager/lib_spec.rb
|
65
|
+
- spec/lib/mailmanager/list_spec.rb
|
66
|
+
- spec/lib/mailmanager_spec.rb
|
67
|
+
- spec/spec_helper.rb
|
68
|
+
- Changelog
|
69
|
+
- LICENSE
|
70
|
+
- README.rdoc
|
71
|
+
has_rdoc: true
|
72
|
+
homepage: http://github.com/dnclabs/mailmanager
|
73
|
+
licenses: []
|
74
|
+
|
75
|
+
post_install_message:
|
76
|
+
rdoc_options: []
|
77
|
+
|
78
|
+
require_paths:
|
79
|
+
- lib
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
segments:
|
86
|
+
- 0
|
87
|
+
version: "0"
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
segments:
|
94
|
+
- 0
|
95
|
+
version: "0"
|
96
|
+
requirements: []
|
97
|
+
|
98
|
+
rubyforge_project:
|
99
|
+
rubygems_version: 1.3.7
|
100
|
+
signing_key:
|
101
|
+
specification_version: 3
|
102
|
+
summary: GNU Mailman wrapper for Ruby
|
103
|
+
test_files: []
|
104
|
+
|