ezmlm 1.0.0

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