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.
- checksums.yaml +7 -0
- data/ext/ezmlm/hash/extconf.rb +5 -0
- data/ext/ezmlm/hash/hash.c +193 -0
- data/ext/ezmlm/hash/hash.h +46 -0
- data/lib/ezmlm.rb +65 -0
- data/lib/ezmlm/list.rb +834 -0
- data/lib/ezmlm/list/author.rb +119 -0
- data/lib/ezmlm/list/message.rb +121 -0
- data/lib/ezmlm/list/thread.rb +117 -0
- metadata +86 -0
checksums.yaml
ADDED
@@ -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,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
|
+
* callseq:
|
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
|
+
* callseq:
|
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
|
+
|
data/lib/ezmlm.rb
ADDED
@@ -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
|
+
|
data/lib/ezmlm/list.rb
ADDED
@@ -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
|
+
|