ezmlm 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|