mailmanager 1.0.8
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/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
|
+
|