author | Mahlon E. Smith <mahlon@martini.nu> |
Thu, 03 Oct 2019 11:44:44 -0700 | |
changeset 28 | 99fdbf4a5f37 |
parent 26 | a89d91d4b157 |
permissions | -rw-r--r-- |
1
1d3cfd4837a8
Filled out the project, added Ezmlm module + spec.
Michael Granger <mgranger@laika.com>
parents:
diff
changeset
|
1 |
#!/usr/bin/ruby |
12
3cc813140c80
First round of modernizing after a long absence.
Mahlon E. Smith <mahlon@martini.nu>
parents:
5
diff
changeset
|
2 |
# vim: set nosta noet ts=4 sw=4: |
17 | 3 |
|
1
1d3cfd4837a8
Filled out the project, added Ezmlm module + spec.
Michael Granger <mgranger@laika.com>
parents:
diff
changeset
|
4 |
require 'pathname' |
15 | 5 |
require 'time' |
13 | 6 |
require 'etc' |
15 | 7 |
require 'ezmlm' unless defined?( Ezmlm ) |
1
1d3cfd4837a8
Filled out the project, added Ezmlm module + spec.
Michael Granger <mgranger@laika.com>
parents:
diff
changeset
|
8 |
|
1d3cfd4837a8
Filled out the project, added Ezmlm module + spec.
Michael Granger <mgranger@laika.com>
parents:
diff
changeset
|
9 |
|
20
9d59d30685cb
Fixes for documentation and test directories that didn't make it into the repo.
Mahlon E. Smith <mahlon@laika.com>
parents:
17
diff
changeset
|
10 |
# A Ruby interface to a single Ezmlm-idx mailing list directory. |
9d59d30685cb
Fixes for documentation and test directories that didn't make it into the repo.
Mahlon E. Smith <mahlon@laika.com>
parents:
17
diff
changeset
|
11 |
# |
9d59d30685cb
Fixes for documentation and test directories that didn't make it into the repo.
Mahlon E. Smith <mahlon@laika.com>
parents:
17
diff
changeset
|
12 |
# list = Ezmlm::List.new( '/path/to/listdir' ) |
9d59d30685cb
Fixes for documentation and test directories that didn't make it into the repo.
Mahlon E. Smith <mahlon@laika.com>
parents:
17
diff
changeset
|
13 |
# |
9d59d30685cb
Fixes for documentation and test directories that didn't make it into the repo.
Mahlon E. Smith <mahlon@laika.com>
parents:
17
diff
changeset
|
14 |
#--- |
1
1d3cfd4837a8
Filled out the project, added Ezmlm module + spec.
Michael Granger <mgranger@laika.com>
parents:
diff
changeset
|
15 |
class Ezmlm::List |
28
99fdbf4a5f37
Be explicit with sticky removal, so two simultaneous processes don't create a race where sticky is left enabled.
Mahlon E. Smith <mahlon@martini.nu>
parents:
26
diff
changeset
|
16 |
# $Id: list.rb,v a89d91d4b157 2017/06/23 17:54:26 mahlon $ |
12
3cc813140c80
First round of modernizing after a long absence.
Mahlon E. Smith <mahlon@martini.nu>
parents:
5
diff
changeset
|
17 |
|
13 | 18 |
# Valid subdirectories/sections for subscriptions. |
19 |
SUBSCRIPTION_DIRS = %w[ deny mod digest allow ] |
|
20 |
||
21 |
||
2
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
22 |
### Create a new Ezmlm::List object for the specified +listdir+, which should be |
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
23 |
### an ezmlm-idx mailing list directory. |
12
3cc813140c80
First round of modernizing after a long absence.
Mahlon E. Smith <mahlon@martini.nu>
parents:
5
diff
changeset
|
24 |
### |
2
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
25 |
def initialize( listdir ) |
12
3cc813140c80
First round of modernizing after a long absence.
Mahlon E. Smith <mahlon@martini.nu>
parents:
5
diff
changeset
|
26 |
listdir = Pathname.new( listdir ) unless listdir.is_a?( Pathname ) |
25
81cc7d47f68f
Oop, the 'mailinglist' file was leftover from older ezmlm. Not created by ezmlm-make anymore.
Mahlon E. Smith <mahlon@martini.nu>
parents:
24
diff
changeset
|
27 |
unless listdir.directory? && ( listdir + 'ezmlmrc' ).exist? |
17 | 28 |
raise ArgumentError, "%p doesn't appear to be an ezmlm-idx list." % [ listdir.to_s ] |
29 |
end |
|
2
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
30 |
@listdir = listdir |
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
31 |
end |
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
32 |
|
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
33 |
# The Pathname object for the list directory |
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
34 |
attr_reader :listdir |
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
35 |
|
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
36 |
|
5
804e1c2b9a40
* Added rdoc-generation to the cruise task and the artifacts that get saved
Michael Granger <mgranger@laika.com>
parents:
4
diff
changeset
|
37 |
### Return the configured name of the list (without the host) |
13 | 38 |
### |
5
804e1c2b9a40
* Added rdoc-generation to the cruise task and the artifacts that get saved
Michael Granger <mgranger@laika.com>
parents:
4
diff
changeset
|
39 |
def name |
13 | 40 |
@name = self.read( 'outlocal' ) unless @name |
41 |
return @name |
|
5
804e1c2b9a40
* Added rdoc-generation to the cruise task and the artifacts that get saved
Michael Granger <mgranger@laika.com>
parents:
4
diff
changeset
|
42 |
end |
12
3cc813140c80
First round of modernizing after a long absence.
Mahlon E. Smith <mahlon@martini.nu>
parents:
5
diff
changeset
|
43 |
|
5
804e1c2b9a40
* Added rdoc-generation to the cruise task and the artifacts that get saved
Michael Granger <mgranger@laika.com>
parents:
4
diff
changeset
|
44 |
|
804e1c2b9a40
* Added rdoc-generation to the cruise task and the artifacts that get saved
Michael Granger <mgranger@laika.com>
parents:
4
diff
changeset
|
45 |
### Return the configured host of the list |
13 | 46 |
### |
5
804e1c2b9a40
* Added rdoc-generation to the cruise task and the artifacts that get saved
Michael Granger <mgranger@laika.com>
parents:
4
diff
changeset
|
47 |
def host |
13 | 48 |
@host = self.read( 'outhost' ) unless @host |
49 |
return @host |
|
5
804e1c2b9a40
* Added rdoc-generation to the cruise task and the artifacts that get saved
Michael Granger <mgranger@laika.com>
parents:
4
diff
changeset
|
50 |
end |
12
3cc813140c80
First round of modernizing after a long absence.
Mahlon E. Smith <mahlon@martini.nu>
parents:
5
diff
changeset
|
51 |
|
5
804e1c2b9a40
* Added rdoc-generation to the cruise task and the artifacts that get saved
Michael Granger <mgranger@laika.com>
parents:
4
diff
changeset
|
52 |
|
804e1c2b9a40
* Added rdoc-generation to the cruise task and the artifacts that get saved
Michael Granger <mgranger@laika.com>
parents:
4
diff
changeset
|
53 |
### Return the configured address of the list (in list@host form) |
13 | 54 |
### |
5
804e1c2b9a40
* Added rdoc-generation to the cruise task and the artifacts that get saved
Michael Granger <mgranger@laika.com>
parents:
4
diff
changeset
|
55 |
def address |
804e1c2b9a40
* Added rdoc-generation to the cruise task and the artifacts that get saved
Michael Granger <mgranger@laika.com>
parents:
4
diff
changeset
|
56 |
return "%s@%s" % [ self.name, self.host ] |
804e1c2b9a40
* Added rdoc-generation to the cruise task and the artifacts that get saved
Michael Granger <mgranger@laika.com>
parents:
4
diff
changeset
|
57 |
end |
804e1c2b9a40
* Added rdoc-generation to the cruise task and the artifacts that get saved
Michael Granger <mgranger@laika.com>
parents:
4
diff
changeset
|
58 |
alias_method :fullname, :address |
12
3cc813140c80
First round of modernizing after a long absence.
Mahlon E. Smith <mahlon@martini.nu>
parents:
5
diff
changeset
|
59 |
|
5
804e1c2b9a40
* Added rdoc-generation to the cruise task and the artifacts that get saved
Michael Granger <mgranger@laika.com>
parents:
4
diff
changeset
|
60 |
|
13 | 61 |
### Return the email address of the list's owner. |
62 |
### |
|
63 |
def owner |
|
64 |
owner = self.read( 'owner' ) |
|
65 |
return owner =~ /@/ ? owner : nil |
|
66 |
end |
|
67 |
||
68 |
||
14 | 69 |
### Returns +true+ if +address+ is a subscriber to this list. |
13 | 70 |
### |
14 | 71 |
def include?( addr, section: nil ) |
26
a89d91d4b157
Don't mutate the caller's argument.
Mahlon E. Smith <mahlon@martini.nu>
parents:
25
diff
changeset
|
72 |
addr = addr.downcase |
16
e135ccae6783
Migrate hashing functions to C.
Mahlon E. Smith <mahlon@laika.com>
parents:
15
diff
changeset
|
73 |
file = self.subscription_dir( section ) + Ezmlm::Hash.subscriber( addr ) |
14 | 74 |
return false unless file.exist? |
75 |
return file.read.scan( /T([^\0]+)\0/ ).flatten.include?( addr ) |
|
13 | 76 |
end |
14 | 77 |
alias_method :is_subscriber?, :include? |
13 | 78 |
|
79 |
||
80 |
### Fetch a sorted Array of the email addresses for all of the list's |
|
81 |
### subscribers. |
|
82 |
### |
|
83 |
def subscribers |
|
84 |
return self.read_subscriber_dir |
|
85 |
end |
|
86 |
||
87 |
||
88 |
### Subscribe +addr+ to the list within +section+. |
|
89 |
### |
|
90 |
def subscribe( *addr, section: nil ) |
|
91 |
addr.each do |address| |
|
92 |
next unless address.index( '@' ) |
|
26
a89d91d4b157
Don't mutate the caller's argument.
Mahlon E. Smith <mahlon@martini.nu>
parents:
25
diff
changeset
|
93 |
address = address.downcase |
13 | 94 |
|
16
e135ccae6783
Migrate hashing functions to C.
Mahlon E. Smith <mahlon@laika.com>
parents:
15
diff
changeset
|
95 |
file = self.subscription_dir( section ) + Ezmlm::Hash.subscriber( address ) |
13 | 96 |
self.with_safety do |
97 |
if file.exist? |
|
98 |
addresses = file.read.scan( /T([^\0]+)\0/ ).flatten |
|
99 |
addresses << address |
|
100 |
file.open( 'w' ) do |f| |
|
101 |
f.print addresses.uniq.sort.map{|a| "T#{a}\0" }.join |
|
102 |
end |
|
103 |
||
104 |
else |
|
105 |
file.open( 'w' ) do |f| |
|
106 |
f.print "T%s\0" % [ address ] |
|
107 |
end |
|
108 |
end |
|
109 |
end |
|
110 |
end |
|
111 |
end |
|
14 | 112 |
alias_method :add_subscriber, :subscribe |
13 | 113 |
|
114 |
||
115 |
### Unsubscribe +addr+ from the list within +section+. |
|
116 |
### |
|
117 |
def unsubscribe( *addr, section: nil ) |
|
118 |
addr.each do |address| |
|
26
a89d91d4b157
Don't mutate the caller's argument.
Mahlon E. Smith <mahlon@martini.nu>
parents:
25
diff
changeset
|
119 |
address = address.downcase |
13 | 120 |
|
16
e135ccae6783
Migrate hashing functions to C.
Mahlon E. Smith <mahlon@laika.com>
parents:
15
diff
changeset
|
121 |
file = self.subscription_dir( section ) + Ezmlm::Hash.subscriber( address ) |
13 | 122 |
self.with_safety do |
123 |
next unless file.exist? |
|
124 |
addresses = file.read.scan( /T([^\0]+)\0/ ).flatten |
|
125 |
addresses = addresses - [ address ] |
|
126 |
||
127 |
if addresses.empty? |
|
128 |
file.unlink |
|
129 |
else |
|
130 |
file.open( 'w' ) do |f| |
|
131 |
f.print addresses.uniq.sort.map{|a| "T#{a}\0" }.join |
|
132 |
end |
|
133 |
end |
|
134 |
end |
|
135 |
end |
|
136 |
end |
|
14 | 137 |
alias_method :remove_subscriber, :unsubscribe |
138 |
||
139 |
||
140 |
### Returns an Array of email addresses of people responsible for |
|
141 |
### moderating subscription of a closed list. |
|
142 |
### |
|
143 |
def moderators |
|
144 |
return self.read_subscriber_dir( 'mod' ) |
|
145 |
end |
|
146 |
||
147 |
### Returns +true+ if +address+ is a moderator. |
|
148 |
### |
|
149 |
def is_moderator?( addr ) |
|
150 |
return self.include?( addr, section: 'mod' ) |
|
151 |
end |
|
152 |
||
153 |
### Subscribe +addr+ to the list as a Moderator. |
|
154 |
### |
|
155 |
def add_moderator( *addr ) |
|
156 |
return self.subscribe( *addr, section: 'mod' ) |
|
157 |
end |
|
158 |
||
159 |
### Remove +addr+ from the list as a Moderator. |
|
160 |
### |
|
161 |
def remove_moderator( *addr ) |
|
162 |
return self.unsubscribe( *addr, section: 'mod' ) |
|
163 |
end |
|
13 | 164 |
|
165 |
||
14 | 166 |
### Returns an Array of email addresses denied access |
167 |
### to the list. |
|
168 |
### |
|
169 |
def blacklisted |
|
170 |
return self.read_subscriber_dir( 'deny' ) |
|
171 |
end |
|
172 |
||
173 |
### Returns +true+ if +address+ is disallowed from participating. |
|
174 |
### |
|
175 |
def is_blacklisted?( addr ) |
|
176 |
return self.include?( addr, section: 'deny' ) |
|
177 |
end |
|
178 |
||
179 |
### Blacklist +addr+ from the list. |
|
180 |
### |
|
181 |
def add_blacklisted( *addr ) |
|
182 |
return self.subscribe( *addr, section: 'deny' ) |
|
183 |
end |
|
184 |
||
185 |
### Remove +addr+ from the blacklist. |
|
186 |
### |
|
187 |
def remove_blacklisted( *addr ) |
|
188 |
return self.unsubscribe( *addr, section: 'deny' ) |
|
189 |
end |
|
190 |
||
191 |
||
192 |
||
193 |
### Returns an Array of email addresses that act like |
|
194 |
### regular subscribers for user-post only lists. |
|
13 | 195 |
### |
14 | 196 |
def allowed |
197 |
return self.read_subscriber_dir( 'allow' ) |
|
198 |
end |
|
199 |
||
200 |
### Returns +true+ if +address+ is given the same benefits as a |
|
201 |
### regular subscriber for user-post only lists. |
|
202 |
### |
|
203 |
def is_allowed?( addr ) |
|
204 |
return self.include?( addr, section: 'allow' ) |
|
205 |
end |
|
206 |
||
207 |
### Add +addr+ to allow posting to user-post only lists, |
|
208 |
### when +addr+ isn't a subscriber. |
|
209 |
### |
|
210 |
def add_allowed( *addr ) |
|
211 |
return self.subscribe( *addr, section: 'allow' ) |
|
212 |
end |
|
213 |
||
214 |
### Remove +addr+ from the allowed list. |
|
215 |
### |
|
216 |
def remove_allowed( *addr ) |
|
217 |
return self.unsubscribe( *addr, section: 'allow' ) |
|
218 |
end |
|
219 |
||
220 |
||
221 |
### Returns +true+ if the list is configured to respond |
|
15 | 222 |
### to remote management requests. |
14 | 223 |
### |
224 |
def public? |
|
225 |
return ( self.listdir + 'public' ).exist? |
|
226 |
end |
|
227 |
||
228 |
### Disable or enable remote management requests. |
|
229 |
### |
|
230 |
def public=( enable=true ) |
|
231 |
if enable |
|
232 |
self.touch( 'public' ) |
|
233 |
else |
|
234 |
self.unlink( 'public' ) |
|
235 |
end |
|
236 |
end |
|
15 | 237 |
alias_method :public, :public= |
14 | 238 |
|
239 |
### Returns +true+ if the list is not configured to respond |
|
15 | 240 |
### to remote management requests. |
13 | 241 |
### |
14 | 242 |
def private? |
243 |
return ! self.public? |
|
244 |
end |
|
245 |
||
246 |
### Disable or enable remote management requests. |
|
247 |
### |
|
248 |
def private=( enable=false ) |
|
249 |
self.public = ! enable |
|
250 |
end |
|
15 | 251 |
alias_method :private, :private= |
14 | 252 |
|
253 |
||
254 |
### Returns +true+ if the list supports remote administration |
|
255 |
### subscribe/unsubscribe requests from moderators. |
|
256 |
### |
|
257 |
def remote_subscriptions? |
|
258 |
return ( self.listdir + 'remote' ).exist? |
|
259 |
end |
|
260 |
||
261 |
### Disable or enable remote subscription requests. |
|
262 |
### |
|
263 |
def remote_subscriptions=( enable=false ) |
|
264 |
if enable |
|
265 |
self.touch( 'remote' ) |
|
266 |
else |
|
267 |
self.unlink( 'remote' ) |
|
268 |
end |
|
4
8c4ae0797d5f
Added more archive-related functions:
Michael Granger <mgranger@laika.com>
parents:
2
diff
changeset
|
269 |
end |
15 | 270 |
alias_method :remote_subscriptions, :remote_subscriptions= |
5
804e1c2b9a40
* Added rdoc-generation to the cruise task and the artifacts that get saved
Michael Granger <mgranger@laika.com>
parents:
4
diff
changeset
|
271 |
|
804e1c2b9a40
* Added rdoc-generation to the cruise task and the artifacts that get saved
Michael Granger <mgranger@laika.com>
parents:
4
diff
changeset
|
272 |
|
14 | 273 |
### Returns +true+ if list subscription requests require moderator |
274 |
### approval. |
|
275 |
### |
|
276 |
def moderated_subscriptions? |
|
277 |
return ( self.listdir + 'modsub' ).exist? |
|
278 |
end |
|
279 |
||
280 |
### Disable or enable subscription moderation. |
|
281 |
### |
|
282 |
def moderated_subscriptions=( enable=false ) |
|
283 |
if enable |
|
284 |
self.touch( 'modsub' ) |
|
285 |
else |
|
286 |
self.unlink( 'modsub' ) |
|
287 |
end |
|
288 |
end |
|
15 | 289 |
alias_method :moderated_subscriptions, :moderated_subscriptions= |
14 | 290 |
|
291 |
### Returns +true+ if message moderation is enabled. |
|
292 |
### |
|
293 |
def moderated? |
|
294 |
return ( self.listdir + 'modpost' ).exist? |
|
295 |
end |
|
296 |
||
297 |
### Disable or enable message moderation. |
|
298 |
### |
|
15 | 299 |
### This has special meaning when combined with user_posts_only setting. |
14 | 300 |
### Lists act as unmoderated for subscribers, and posts from unknown |
301 |
### addresses go to moderation. |
|
13 | 302 |
### |
14 | 303 |
def moderated=( enable=false ) |
304 |
if enable |
|
305 |
self.touch( 'modpost' ) |
|
306 |
self.touch( 'noreturnposts' ) if self.user_posts_only? |
|
307 |
else |
|
308 |
self.unlink( 'modpost' ) |
|
309 |
self.unlink( 'noreturnposts' ) if self.user_posts_only? |
|
310 |
end |
|
311 |
end |
|
15 | 312 |
alias_method :moderated, :moderated= |
14 | 313 |
|
314 |
||
315 |
### Returns +true+ if posting is only allowed by moderators. |
|
316 |
### |
|
317 |
def moderator_posts_only? |
|
318 |
return ( self.listdir + 'modpostonly' ).exist? |
|
319 |
end |
|
320 |
||
321 |
### Disable or enable moderation only posts. |
|
322 |
### |
|
323 |
def moderator_posts_only=( enable=false ) |
|
324 |
if enable |
|
325 |
self.touch( 'modpostonly' ) |
|
326 |
else |
|
327 |
self.unlink( 'modpostonly' ) |
|
328 |
end |
|
329 |
end |
|
15 | 330 |
alias_method :moderator_posts_only, :moderator_posts_only= |
14 | 331 |
|
332 |
||
333 |
### Returns +true+ if posting is only allowed by subscribers. |
|
334 |
### |
|
335 |
def user_posts_only? |
|
336 |
return ( self.listdir + 'subpostonly' ).exist? |
|
337 |
end |
|
338 |
||
339 |
### Disable or enable user only posts. |
|
340 |
### This is easily defeated, moderated lists are preferred. |
|
341 |
### |
|
342 |
### This has special meaning for moderated lists. Lists act as |
|
343 |
### unmoderated for subscribers, and posts from unknown addresses |
|
344 |
### go to moderation. |
|
345 |
### |
|
346 |
def user_posts_only=( enable=false ) |
|
347 |
if enable |
|
348 |
self.touch( 'subpostonly' ) |
|
349 |
self.touch( 'noreturnposts' )if self.moderated? |
|
350 |
else |
|
351 |
self.unlink( 'subpostonly' ) |
|
352 |
self.unlink( 'noreturnposts' ) if self.moderated? |
|
353 |
end |
|
2
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
354 |
end |
15 | 355 |
alias_method :user_posts_only, :user_posts_only= |
2
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
356 |
|
14 | 357 |
|
358 |
### Returns +true+ if message archival is enabled. |
|
359 |
### |
|
360 |
def archived? |
|
17 | 361 |
test = %w[ archived indexed threaded ].each_with_object( [] ) do |f, acc| |
362 |
acc << self.listdir + f |
|
363 |
end |
|
364 |
||
365 |
return test.all?( &:exist? ) |
|
14 | 366 |
end |
367 |
||
17 | 368 |
### Disable or enable message archiving (and indexing/threading.) |
14 | 369 |
### |
17 | 370 |
def archived=( enable=true ) |
14 | 371 |
if enable |
17 | 372 |
self.touch( 'archived', 'indexed', 'threaded' ) |
14 | 373 |
else |
17 | 374 |
self.unlink( 'archived', 'indexed', 'threaded' ) |
14 | 375 |
end |
376 |
end |
|
17 | 377 |
alias_method :archived, :archived= |
14 | 378 |
|
379 |
### Returns +true+ if the message archive is accessible only to |
|
380 |
### moderators. |
|
381 |
### |
|
382 |
def private_archive? |
|
383 |
return ( self.listdir + 'modgetonly' ).exist? |
|
384 |
end |
|
385 |
||
386 |
### Disable or enable private access to the archive. |
|
387 |
### |
|
388 |
def private_archive=( enable=true ) |
|
389 |
if enable |
|
390 |
self.touch( 'modgetonly' ) |
|
391 |
else |
|
392 |
self.unlink( 'modgetonly' ) |
|
393 |
end |
|
394 |
end |
|
15 | 395 |
alias_method :private_archive, :private_archive= |
14 | 396 |
|
397 |
### Returns +true+ if the message archive is accessible to anyone. |
|
398 |
### |
|
399 |
def public_archive? |
|
400 |
return ! self.private_archive? |
|
401 |
end |
|
402 |
||
15 | 403 |
### Disable or enable private access to the archive. |
404 |
### |
|
405 |
def public_archive=( enable=true ) |
|
406 |
self.private_archive = ! enable |
|
407 |
end |
|
408 |
alias_method :public_archive, :public_archive= |
|
409 |
||
14 | 410 |
### Returns +true+ if the message archive is accessible only to |
411 |
### list subscribers. |
|
412 |
### |
|
413 |
def guarded_archive? |
|
414 |
return ( self.listdir + 'subgetonly' ).exist? |
|
415 |
end |
|
416 |
||
417 |
### Disable or enable loimited access to the archive. |
|
418 |
### |
|
419 |
def guarded_archive=( enable=true ) |
|
420 |
if enable |
|
421 |
self.touch( 'subgetonly' ) |
|
422 |
else |
|
423 |
self.unlink( 'subgetonly' ) |
|
424 |
end |
|
425 |
end |
|
15 | 426 |
alias_method :guarded_archive, :guarded_archive= |
14 | 427 |
|
428 |
||
429 |
### Returns +true+ if message digests are enabled. |
|
13 | 430 |
### |
14 | 431 |
def digested? |
432 |
return ( self.listdir + 'digested' ).exist? |
|
433 |
end |
|
434 |
||
435 |
### Disable or enable message digesting. |
|
436 |
### |
|
437 |
def digest=( enable=true ) |
|
438 |
if enable |
|
439 |
self.touch( 'digested' ) |
|
440 |
else |
|
441 |
self.unlink( 'digested' ) |
|
442 |
end |
|
443 |
end |
|
15 | 444 |
alias_method :digest, :digest= |
14 | 445 |
|
446 |
### If the list is digestable, trigger the digest after this amount |
|
447 |
### of message body since the latest digest, in kbytes. |
|
448 |
### |
|
449 |
### See: ezmlm-tstdig(1) |
|
450 |
### |
|
451 |
def digest_kbytesize |
|
452 |
size = self.read( 'digsize' ).to_i |
|
453 |
return size.zero? ? 64 : size |
|
454 |
end |
|
455 |
||
456 |
### If the list is digestable, trigger the digest after this amount |
|
457 |
### of message body since the latest digest, in kbytes. |
|
458 |
### |
|
459 |
### See: ezmlm-tstdig(1) |
|
460 |
### |
|
461 |
def digest_kbytesize=( size=64 ) |
|
462 |
self.write( 'digsize' ) {|f| f.puts size.to_i } |
|
463 |
end |
|
464 |
||
465 |
### If the list is digestable, trigger the digest after this many |
|
466 |
### messages have accumulated since the latest digest. |
|
467 |
### |
|
468 |
### See: ezmlm-tstdig(1) |
|
469 |
### |
|
470 |
def digest_count |
|
471 |
count = self.read( 'digcount' ).to_i |
|
472 |
return count.zero? ? 30 : count |
|
473 |
end |
|
474 |
||
475 |
### If the list is digestable, trigger the digest after this many |
|
476 |
### messages have accumulated since the latest digest. |
|
477 |
### |
|
478 |
### See: ezmlm-tstdig(1) |
|
479 |
### |
|
480 |
def digest_count=( count=30 ) |
|
481 |
self.write( 'digcount' ) {|f| f.puts count.to_i } |
|
482 |
end |
|
483 |
||
484 |
### If the list is digestable, trigger the digest after this much |
|
485 |
### time has passed since the last digest, in hours. |
|
486 |
### |
|
487 |
### See: ezmlm-tstdig(1) |
|
488 |
### |
|
489 |
def digest_timeout |
|
490 |
hours = self.read( 'digtime' ).to_i |
|
491 |
return hours.zero? ? 48 : hours |
|
492 |
end |
|
493 |
||
494 |
### If the list is digestable, trigger the digest after this much |
|
495 |
### time has passed since the last digest, in hours. |
|
496 |
### |
|
497 |
### See: ezmlm-tstdig(1) |
|
498 |
### |
|
499 |
def digest_timeout=( hours=48 ) |
|
500 |
self.write( 'digtime' ) {|f| f.puts hours.to_i } |
|
2
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
501 |
end |
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
502 |
|
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
503 |
|
14 | 504 |
### Returns +true+ if the list requires subscriptions to be |
505 |
### confirmed. AKA "help" mode if disabled. |
|
506 |
### |
|
507 |
def confirm_subscriptions? |
|
508 |
return ! ( self.listdir + 'nosubconfirm' ).exist? |
|
509 |
end |
|
510 |
||
511 |
### Disable or enable subscription confirmation. |
|
512 |
### AKA "help" mode if disabled. |
|
513 |
### |
|
514 |
def confirm_subscriptions=( enable=true ) |
|
515 |
if enable |
|
516 |
self.unlink( 'nosubconfirm' ) |
|
517 |
else |
|
518 |
self.touch( 'nosubconfirm' ) |
|
519 |
end |
|
520 |
end |
|
15 | 521 |
alias_method :confirm_subscriptions, :confirm_subscriptions= |
14 | 522 |
|
523 |
### Returns +true+ if the list requires unsubscriptions to be |
|
524 |
### confirmed. AKA "jump" mode. |
|
525 |
### |
|
526 |
def confirm_unsubscriptions? |
|
527 |
return ! ( self.listdir + 'nounsubconfirm' ).exist? |
|
528 |
end |
|
529 |
||
530 |
### Disable or enable unsubscription confirmation. |
|
531 |
### AKA "jump" mode. |
|
532 |
### |
|
533 |
def confirm_unsubscriptions=( enable=true ) |
|
534 |
if enable |
|
535 |
self.unlink( 'nounsubconfirm' ) |
|
536 |
else |
|
537 |
self.touch( 'nounsubconfirm' ) |
|
538 |
end |
|
539 |
end |
|
15 | 540 |
alias_method :confirm_unsubscriptions, :confirm_unsubscriptions= |
14 | 541 |
|
542 |
||
543 |
### Returns +true+ if the list requires regular message postings |
|
544 |
### to be confirmed by the original sender. |
|
545 |
### |
|
546 |
def confirm_postings? |
|
547 |
return ( self.listdir + 'confirmpost' ).exist? |
|
548 |
end |
|
549 |
||
550 |
### Disable or enable message confirmation. |
|
551 |
### |
|
552 |
def confirm_postings=( enable=false ) |
|
553 |
if enable |
|
554 |
self.touch( 'confirmpost' ) |
|
555 |
else |
|
556 |
self.unlink( 'confirmpost' ) |
|
557 |
end |
|
558 |
end |
|
15 | 559 |
alias_method :confirm_postings, :confirm_postings= |
14 | 560 |
|
561 |
||
562 |
### Returns +true+ if the list allows moderators to |
|
563 |
### fetch a subscriber list remotely. |
|
564 |
### |
|
565 |
def allow_remote_listing? |
|
566 |
return ( self.listdir + 'modcanlist' ).exist? |
|
567 |
end |
|
568 |
||
569 |
### Disable or enable the ability for moderators to |
|
570 |
### remotely fetch a subscriber list. |
|
571 |
### |
|
572 |
def allow_remote_listing=( enable=false ) |
|
573 |
if enable |
|
574 |
self.touch( 'modcanlist' ) |
|
575 |
else |
|
576 |
self.unlink( 'modcanlist' ) |
|
577 |
end |
|
578 |
end |
|
15 | 579 |
alias_method :allow_remote_listing, :allow_remote_listing= |
14 | 580 |
|
581 |
||
582 |
### Returns +true+ if the list automatically manages |
|
583 |
### bouncing subscriber addresses. |
|
584 |
### |
|
585 |
def bounce_warnings? |
|
586 |
return ! ( self.listdir + 'nowarn' ).exist? |
|
587 |
end |
|
588 |
||
589 |
### Disable or enable automatic bounce probes and warnings. |
|
590 |
### |
|
591 |
def bounce_warnings=( enable=true ) |
|
592 |
if enable |
|
593 |
self.unlink( 'nowarn' ) |
|
594 |
else |
|
595 |
self.touch( 'nowarn' ) |
|
596 |
end |
|
597 |
end |
|
15 | 598 |
alias_method :bounce_warnings, :bounce_warnings= |
14 | 599 |
|
600 |
||
601 |
### Return the maximum message size, in bytes. Messages larger than |
|
602 |
### this size will be rejected. |
|
603 |
### |
|
604 |
### See: ezmlm-reject(1) |
|
605 |
### |
|
606 |
def maximum_message_size |
|
607 |
size = self.read( 'msgsize' ) |
|
608 |
return size ? size.split( ':' ).first.to_i : 0 |
|
609 |
end |
|
610 |
||
611 |
### Set the maximum message size, in bytes. Messages larger than |
|
15 | 612 |
### this size will be rejected. Defaults to 300kb. |
14 | 613 |
### |
614 |
### See: ezmlm-reject(1) |
|
615 |
### |
|
616 |
def maximum_message_size=( size=307200 ) |
|
617 |
if size.to_i.zero? |
|
618 |
self.unlink( 'msgsize' ) |
|
619 |
else |
|
620 |
self.write( 'msgsize' ) {|f| f.puts "#{size.to_i}:0" } |
|
621 |
end |
|
622 |
end |
|
623 |
||
624 |
||
15 | 625 |
|
14 | 626 |
### Return the number of messages in the list archive. |
627 |
### |
|
628 |
def message_count |
|
629 |
count = self.read( 'archnum' ) |
|
630 |
return count ? Integer( count ) : 0 |
|
631 |
end |
|
632 |
||
15 | 633 |
### Returns an individual message if archiving was enabled. |
634 |
### |
|
635 |
def message( message_id ) |
|
636 |
raise "Message archive is empty." if self.message_count.zero? |
|
17 | 637 |
return Ezmlm::List::Message.new( self, message_id ) rescue nil |
15 | 638 |
end |
639 |
||
640 |
### Lazy load each message ID as a Ezmlm::List::Message, |
|
641 |
### yielding it to the block. |
|
13 | 642 |
### |
15 | 643 |
def each_message |
644 |
( 1 .. self.message_count ).each do |id| |
|
645 |
yield self.message( id ) |
|
646 |
end |
|
647 |
end |
|
648 |
||
649 |
||
650 |
### Return a Thread object for the given +thread_id+. |
|
651 |
### |
|
652 |
def thread( thread_id ) |
|
17 | 653 |
return Ezmlm::List::Thread.new( self, thread_id ) rescue nil |
15 | 654 |
end |
655 |
||
656 |
||
16
e135ccae6783
Migrate hashing functions to C.
Mahlon E. Smith <mahlon@laika.com>
parents:
15
diff
changeset
|
657 |
### Return an Author object for the given +author_id+, which |
e135ccae6783
Migrate hashing functions to C.
Mahlon E. Smith <mahlon@laika.com>
parents:
15
diff
changeset
|
658 |
### could also be an email address. |
15 | 659 |
### |
660 |
def author( author_id ) |
|
16
e135ccae6783
Migrate hashing functions to C.
Mahlon E. Smith <mahlon@laika.com>
parents:
15
diff
changeset
|
661 |
author_id = Ezmlm::Hash.address(author_id) if author_id.index( '@' ) |
17 | 662 |
return Ezmlm::List::Author.new( self, author_id ) rescue nil |
15 | 663 |
end |
664 |
||
4
8c4ae0797d5f
Added more archive-related functions:
Michael Granger <mgranger@laika.com>
parents:
2
diff
changeset
|
665 |
|
24
f8873b391f3d
Add a "last activity" method to quickly determine when a list was last used. Bump version.
Mahlon E. Smith <mahlon@laika.com>
parents:
20
diff
changeset
|
666 |
### Return a Time object for the last activity on the list, or nil |
f8873b391f3d
Add a "last activity" method to quickly determine when a list was last used. Bump version.
Mahlon E. Smith <mahlon@laika.com>
parents:
20
diff
changeset
|
667 |
### if archiving is disabled or there are no posts. |
f8873b391f3d
Add a "last activity" method to quickly determine when a list was last used. Bump version.
Mahlon E. Smith <mahlon@laika.com>
parents:
20
diff
changeset
|
668 |
### |
f8873b391f3d
Add a "last activity" method to quickly determine when a list was last used. Bump version.
Mahlon E. Smith <mahlon@laika.com>
parents:
20
diff
changeset
|
669 |
def last_activity |
f8873b391f3d
Add a "last activity" method to quickly determine when a list was last used. Bump version.
Mahlon E. Smith <mahlon@laika.com>
parents:
20
diff
changeset
|
670 |
file = self.listdir + 'archnum' |
f8873b391f3d
Add a "last activity" method to quickly determine when a list was last used. Bump version.
Mahlon E. Smith <mahlon@laika.com>
parents:
20
diff
changeset
|
671 |
return unless file.exist? |
f8873b391f3d
Add a "last activity" method to quickly determine when a list was last used. Bump version.
Mahlon E. Smith <mahlon@laika.com>
parents:
20
diff
changeset
|
672 |
return file.stat.mtime |
f8873b391f3d
Add a "last activity" method to quickly determine when a list was last used. Bump version.
Mahlon E. Smith <mahlon@laika.com>
parents:
20
diff
changeset
|
673 |
end |
f8873b391f3d
Add a "last activity" method to quickly determine when a list was last used. Bump version.
Mahlon E. Smith <mahlon@laika.com>
parents:
20
diff
changeset
|
674 |
|
f8873b391f3d
Add a "last activity" method to quickly determine when a list was last used. Bump version.
Mahlon E. Smith <mahlon@laika.com>
parents:
20
diff
changeset
|
675 |
|
15 | 676 |
### Parse all thread indexes into a single array that can be used |
677 |
### as a lookup table. |
|
678 |
### |
|
679 |
### These are not expanded into objects, use #message, #thread, |
|
680 |
### and #author to do so. |
|
681 |
### |
|
682 |
def index |
|
683 |
raise "Archiving is not enabled." unless self.archived? |
|
684 |
archivedir = listdir + 'archive' |
|
685 |
||
686 |
idx = ( 0 .. self.message_count / 100 ).each_with_object( [] ) do |dir, acc| |
|
687 |
index = archivedir + dir.to_s + 'index' |
|
688 |
next unless index.exist? |
|
4
8c4ae0797d5f
Added more archive-related functions:
Michael Granger <mgranger@laika.com>
parents:
2
diff
changeset
|
689 |
|
17 | 690 |
index.open( 'r', encoding: Encoding::ISO8859_1 ) do |fh| |
691 |
fh.each_line.lazy.slice_before( /^\d+:/ ).each do |message| |
|
692 |
||
693 |
match = message[0].match( /^(?<message_id>\d+): (?<thread_id>\w+)/ ) |
|
694 |
next unless match |
|
695 |
thread_id = match[ :thread_id ] |
|
15 | 696 |
|
17 | 697 |
match = message[1].match( /^(?<date>[^;]+);(?<author_id>\w+) / ) |
698 |
next unless match |
|
699 |
author_id = match[ :author_id ] |
|
700 |
date = match[ :date ] |
|
4
8c4ae0797d5f
Added more archive-related functions:
Michael Granger <mgranger@laika.com>
parents:
2
diff
changeset
|
701 |
|
17 | 702 |
metadata = { |
703 |
date: Time.parse( date ), |
|
704 |
thread: thread_id, |
|
705 |
author: author_id |
|
706 |
} |
|
707 |
acc << metadata |
|
708 |
end |
|
15 | 709 |
end |
710 |
end |
|
711 |
||
712 |
return idx |
|
4
8c4ae0797d5f
Added more archive-related functions:
Michael Granger <mgranger@laika.com>
parents:
2
diff
changeset
|
713 |
end |
12
3cc813140c80
First round of modernizing after a long absence.
Mahlon E. Smith <mahlon@martini.nu>
parents:
5
diff
changeset
|
714 |
|
2
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
715 |
|
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
716 |
######### |
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
717 |
protected |
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
718 |
######### |
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
719 |
|
13 | 720 |
### Just return the contents of the provided +file+, rooted |
721 |
### in the list directory. |
|
722 |
### |
|
723 |
def read( file ) |
|
724 |
file = self.listdir + file unless file.is_a?( Pathname ) |
|
725 |
return file.read.chomp |
|
726 |
rescue |
|
727 |
nil |
|
728 |
end |
|
729 |
||
730 |
||
14 | 731 |
### Overwrite +file+ safely, yielding the open filehandle to the |
732 |
### block. Set the new file to correct ownership and permissions. |
|
733 |
### |
|
734 |
def write( file, &block ) |
|
735 |
file = self.listdir + file unless file.is_a?( Pathname ) |
|
736 |
self.with_safety do |
|
737 |
file.open( 'w' ) do |f| |
|
738 |
yield( f ) |
|
739 |
end |
|
740 |
||
741 |
stat = self.listdir.stat |
|
742 |
file.chown( stat.uid, stat.gid ) |
|
743 |
file.chmod( 0600 ) |
|
744 |
end |
|
745 |
end |
|
746 |
||
747 |
||
748 |
### Simply create an empty file, safely. |
|
749 |
### |
|
17 | 750 |
def touch( *file ) |
751 |
self.with_safety do |
|
752 |
Array( file ).flatten.each do |f| |
|
753 |
f = self.listdir + f unless f.is_a?( Pathname ) |
|
754 |
f.open( 'w' ) {} |
|
755 |
end |
|
756 |
end |
|
14 | 757 |
end |
758 |
||
759 |
||
760 |
### Delete +file+ safely. |
|
761 |
### |
|
17 | 762 |
def unlink( *file ) |
14 | 763 |
self.with_safety do |
17 | 764 |
Array( file ).flatten.each do |f| |
765 |
f = self.listdir + f unless f.is_a?( Pathname ) |
|
766 |
next unless f.exist? |
|
767 |
f.unlink |
|
768 |
end |
|
14 | 769 |
end |
770 |
end |
|
771 |
||
772 |
||
13 | 773 |
### Return a Pathname to a subscription directory. |
774 |
### |
|
775 |
def subscription_dir( section=nil ) |
|
776 |
if section |
|
14 | 777 |
unless SUBSCRIPTION_DIRS.include?( section ) |
778 |
raise "Invalid subscription dir: %s, must be one of: %s" % [ |
|
779 |
section, |
|
780 |
SUBSCRIPTION_DIRS.join( ', ' ) |
|
781 |
] |
|
782 |
end |
|
13 | 783 |
return self.listdir + section + 'subscribers' |
784 |
else |
|
785 |
return self.listdir + 'subscribers' |
|
786 |
end |
|
787 |
end |
|
788 |
||
789 |
||
14 | 790 |
### Read the hashed subscriber email addresses from the specified |
791 |
### +directory+ and return them in an Array. |
|
13 | 792 |
### |
793 |
def read_subscriber_dir( section=nil ) |
|
794 |
directory = self.subscription_dir( section ) |
|
2
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
795 |
rval = [] |
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
796 |
Pathname.glob( directory + '*' ) do |hashfile| |
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
797 |
rval.push( hashfile.read.scan(/T([^\0]+)\0/) ) |
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
798 |
end |
12
3cc813140c80
First round of modernizing after a long absence.
Mahlon E. Smith <mahlon@martini.nu>
parents:
5
diff
changeset
|
799 |
|
13 | 800 |
return rval.flatten.sort |
801 |
end |
|
802 |
||
803 |
||
804 |
### Return a Pathname object for the list owner's home directory. |
|
805 |
### |
|
806 |
def homedir |
|
807 |
user = Etc.getpwuid( self.listdir.stat.uid ) |
|
808 |
return Pathname( user.dir ) |
|
809 |
end |
|
810 |
||
811 |
||
812 |
### Safely make modifications to a file within a list directory. |
|
813 |
### |
|
814 |
### Mail can come in at any time. Make changes within a list |
|
815 |
### atomic -- if an incoming message hits when a sticky |
|
816 |
### is set, it is deferred to the Qmail queue. |
|
817 |
### |
|
818 |
### - Set sticky bit on the list directory owner's homedir |
|
819 |
### - Make changes with the block |
|
820 |
### - Unset sticky (just back to what it was previously) |
|
821 |
### |
|
822 |
### All writes should be wrapped in this method. |
|
823 |
### |
|
824 |
def with_safety( &block ) |
|
825 |
home = self.homedir |
|
28
99fdbf4a5f37
Be explicit with sticky removal, so two simultaneous processes don't create a race where sticky is left enabled.
Mahlon E. Smith <mahlon@martini.nu>
parents:
26
diff
changeset
|
826 |
home.chmod( home.stat.mode | 01000 ) # enable sticky |
13 | 827 |
yield |
828 |
||
829 |
ensure |
|
28
99fdbf4a5f37
Be explicit with sticky removal, so two simultaneous processes don't create a race where sticky is left enabled.
Mahlon E. Smith <mahlon@martini.nu>
parents:
26
diff
changeset
|
830 |
home.chmod( home.stat.mode & ~01000 ) # disable sticky |
2
7b5a0131d5cd
Added more list-config accessors
Michael Granger <mgranger@laika.com>
parents:
1
diff
changeset
|
831 |
end |
1
1d3cfd4837a8
Filled out the project, added Ezmlm module + spec.
Michael Granger <mgranger@laika.com>
parents:
diff
changeset
|
832 |
|
12
3cc813140c80
First round of modernizing after a long absence.
Mahlon E. Smith <mahlon@martini.nu>
parents:
5
diff
changeset
|
833 |
end # class Ezmlm::List |
3cc813140c80
First round of modernizing after a long absence.
Mahlon E. Smith <mahlon@martini.nu>
parents:
5
diff
changeset
|
834 |