ezmlm 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+