2
from types import StringType
3
from Mailman.Logging.Syslog import syslog
5
from Mailman import mm_cfg
6
from Mailman import Utils
7
from Mailman import Errors
8
from Mailman import MemberAdaptor
9
from Mailman.Bouncer import _BounceInfo
21
mm_cfg.PGSQL_MEMBER_DB_VERBOSE
22
except AttributeError:
23
mm_cfg.PGSQL_MEMBER_DB_VERBOSE = False # default value
27
class PgsqlMemberships(MemberAdaptor.MemberAdaptor):
29
def __init__(self, mlist):
33
# define the table and standard condition reflecting listname
34
# (this is for upwards compatibility with the 'wide' mode and
35
# the formerly fixed name of database table in 'flat' mode)
36
if self.getTableType() is 'flat':
38
self._table = mm_cfg.PGSQL_MEMBER_TABLE_NAME
39
except AttributeError:
40
self._table = 'mailman_pgsql'
41
self._where = "listname = '%s'" %(self.__mlist.internal_name())
43
self._table = self.__mlist.internal_name()
46
# Make sure we always have the table we need...
47
# if mm_cfg.PGSQL_MEMBER_CREATE_TABLE was defined
49
if mm_cfg.PGSQL_MEMBER_CREATE_TABLE:
51
except AttributeError:
54
if mm_cfg.PGSQL_MEMBER_DB_VERBOSE:
55
# Message to indicate successful init.
56
message = "PgsqlMemberships " \
57
+ "initialized with host: %s (%s)" % (
58
mm_cfg.connection.get_host_info(),
59
mm_cfg.connection.get_server_info() )
60
syslog('error', message)
61
syslog('pgsql', message)
75
mm_cfg.connection.close()
78
if mm_cfg.PGSQL_MEMBER_DB_VERBOSE:
79
# Message to indicate successful close.
80
syslog("error", "PgsqlMemberships unloaded" )
81
syslog("pgsql", "PgsqlMemberships unloaded" )
83
# Find out whether we should be using 'flat' or 'wide' table type.
84
# for backwards compatibility, the default is 'wide'
86
if mm_cfg.PGSQL_MEMBER_TABLE_TYPE:
87
if mm_cfg.PGSQL_MEMBER_TABLE_TYPE is 'flat':
94
# Check to see if a connection's still alive. If not, reconnect.
98
if mm_cfg.connection.ping() == 0:
99
return mm_cfg.connection
101
syslog('pgsql', 'connection warning')
104
# Connection failed, or an error, try a hard dis+reconnect.
106
mm_cfg.cursor.close()
108
syslog('error', 'error on mm_cfg.cursor.close()')
112
mm_cfg.connection.close()
114
syslog('error', 'error on mm_cfg.connection.close()')
118
mm_cfg.connection = psycopg2.connect(
119
passwd=mm_cfg.PGSQL_MEMBER_DB_PASS,
120
db=mm_cfg.PGSQL_MEMBER_DB_NAME,
121
user=mm_cfg.PGSQL_MEMBER_DB_USER,
122
host=mm_cfg.PGSQL_MEMBER_DB_HOST)
123
mm_cfg.cursor = mm_cfg.connection.cursor()
124
except pysopg.OperationalError, e:
125
message = "Error connecting to PGSQL database %s (%s): %s" %(
126
mm_cfg.PGSQL_MEMBER_DB_NAME, e.args[0], e.args[1])
127
syslog('error', message)
128
if mm_cfg.PGSQL_MEMBER_DB_VERBOSE:
129
syslog('pgsql', message)
130
# exit? why not sleep(30) and retry?
133
return mm_cfg.connection
137
# create tables (if the option is set in mm_cfg)
138
def createTable(self):
139
if self.getTableType() is 'flat':
141
"""CREATE TABLE IF NOT EXISTS `%s` (
142
listname varchar(100) NOT NULL,
143
address varchar(255) NOT NULL,
144
hide enum('Y','N') NOT NULL default 'N',
145
nomail enum('Y','N') NOT NULL default 'N',
146
ack enum('Y','N') NOT NULL default 'Y',
147
not_metoo enum('Y','N') NOT NULL default 'Y',
148
digest enum('Y','N') NOT NULL default 'N',
149
plain enum('Y','N') NOT NULL default 'N',
150
password varchar(255) NOT NULL default '!',
151
lang varchar(255) NOT NULL default 'en',
152
name varchar(255) default NULL,
153
one_last_digest enum('Y','N') NOT NULL default 'N',
154
user_options bigint(20) NOT NULL default 0,
155
delivery_status INT(10) NOT NULL default 0,
156
topics_userinterest varchar(255) default NULL,
157
delivery_status_timestamp datetime default '0000-00-00 00:00:00',
158
bi_cookie varchar(255) default NULL,
159
bi_score double NOT NULL default '0',
160
bi_noticesleft double NOT NULL default '0',
161
bi_lastnotice date NOT NULL default '0000-00-00',
162
bi_date date NOT NULL default '0000-00-00',
163
PRIMARY KEY (listname, address)
164
) ENGINE=MyISAM""" %(self._table))
167
"""CREATE TABLE IF NOT EXISTS `%s` (
168
address varchar(255) NOT NULL,
169
hide enum('Y','N') NOT NULL default 'N',
170
nomail enum('Y','N') NOT NULL default 'N',
171
ack enum('Y','N') NOT NULL default 'Y',
172
not_metoo enum('Y','N') NOT NULL default 'Y',
173
digest enum('Y','N') NOT NULL default 'N',
174
plain enum('Y','N') NOT NULL default 'N',
175
password varchar(255) NOT NULL default '!',
176
lang varchar(255) NOT NULL default 'en',
177
name varchar(255) default NULL,
178
one_last_digest enum('Y','N') NOT NULL default 'N',
179
user_options bigint(20) NOT NULL default 0,
180
delivery_status int(10) NOT NULL default 0,
181
topics_userinterest varchar(255) default NULL,
182
delivery_status_timestamp datetime DEFAULT '0000-00-00 00:00:00',
183
bi_cookie varchar(255) default NULL,
184
bi_score double NOT NULL default '0',
185
bi_noticesleft double NOT NULL default '0',
186
bi_lastnotice date NOT NULL default '0000-00-00',
187
bi_date date NOT NULL default '0000-00-00',
188
PRIMARY KEY (address)
189
) ENGINE=MyISAM""" %(self._table))
192
# empty the cache (when we touch a value)
195
self._cachedate = time.localtime()[tm_min]
197
# Apply query on list (manages both 'flat' and 'wide' modes)
198
def query(self, query):
199
if mm_cfg.PGSQL_MEMBER_DB_VERBOSE:
200
syslog('pgsql', query)
202
return mm_cfg.cursor.execute (query)
204
# return all members according to a certain condition
205
def queryall(self, query, cache=False):
207
# get the number of rows in the resultset
208
numrows = int(mm_cfg.cursor.rowcount)
211
for x in range(0,numrows):
212
row = mm_cfg.cursor.fetchone()
213
results.append(row[0])
214
# we don't want to cache the whole list for global requests
215
if cache and numrows < 1000:
216
self._cache[row[1]] = row[2:]
219
# select *, cache it, then return only the field that's asked for
220
def select(self, what, where=''):
221
query = "SELECT " + what \
222
+ ",address,name,user_options,delivery_status,lang,digest " \
223
+ "FROM `%s` WHERE %s" %(self._table, self._where)
225
query += " AND %s" %(where)
226
return self.queryall(query + ' ORDER BY address', True)
228
def select_on(self, what, address):
229
if self._cachedate != time.localtime()[tm_min]:
232
a = self._cache[address]
235
elif what == 'user_options':
237
elif what == 'delivery_status':
241
elif what == 'digest':
245
a = self.select(what,
246
"address='%s'" %(self.escape(address)))
249
def update_on(self, what, value, address):
250
if what == 'delivery_status':
251
dst = ", delivery_status_timestamp=NOW() "
254
self.query("UPDATE `%s` " %(self._table)
255
+ ("SET %s = '%s' " %(what, self.escape(value))
257
+ ("WHERE %s " %(self._where))
258
+ ("AND address = '%s'" %(self.escape(address)))))
262
def escape(self, value):
263
# transforms accents into html entities (é)
264
# TODO: find out which language is current (here: assumes iso-8859-1)
265
value = Utils.uncanonstr(value)
267
# add slashes before " and '
268
return psycopg2.escape_string(value)
271
############################### Now the active codes #######
277
def getMembers(self):
278
return self.select('address')
281
def getRegularMemberKeys(self):
282
return self.select('address', "digest = 'N'")
285
def getDigestMemberKeys(self):
286
return self.select('address', "digest = 'Y'")
288
# status (regular/digest) of a member (returns a key - lowercase)
289
def __get_cp_member(self, member):
290
lcmember = member.lower()
291
digest = self.select_on('digest', lcmember)
294
return lcmember, ISDIGEST
296
return lcmember, ISREGULAR
300
def isMember(self, member):
301
member = self.select_on('name', member)
306
# Covered by SQL __get_cp_member()
307
def getMemberKey(self, member):
308
cpaddr, where = self.__get_cp_member(member)
310
raise Errors.NotAMemberError, member
311
return member.lower()
313
# Covered by SQL __get_cp_member()
314
def getMemberCPAddress(self, member):
315
cpaddr, where = self.__get_cp_member(member)
317
raise Errors.NotAMemberError, member
320
# Covered by SQL __get_cp_member()
321
def getMemberCPAddresses(self, members):
322
return [self.__get_cp_member(member)[0] for member in members]
324
# SELECT password FROM <listname> WHERE address = member.lower()
325
def getMemberPassword(self, member):
326
password = self.select_on('password', member)
330
raise Errors.NotAMemberError, member
332
# Covered by SQL getMemberPassword()
333
def authenticateMember(self, member, response):
334
secret = self.getMemberPassword(member)
335
if secret == response:
339
# Covered by SQL isMember()
340
def __assertIsMember(self, member):
341
if not self.isMember(member):
342
raise Errors.NotAMemberError, member
344
def getMemberLanguage(self, member):
345
lang = self.select_on('lang',member)
346
if len(lang) and lang[0] in self.__mlist.GetAvailableLanguages():
349
return self.__mlist.preferred_language
351
# getOptions: different methods for digest and other (bitfield) options
352
def getMemberOption(self, member, flag):
353
self.__assertIsMember(member)
354
if flag == mm_cfg.Digests:
355
cpaddr, where = self.__get_cp_member(member)
356
return where == ISDIGEST
357
options = self.select_on('user_options', member)
359
return not not (options[0] & flag)
362
# new method to gest faster results when searching a user in the admin Gui
363
def getMembersMatching(self, regexp):
364
return self.select('address',
365
"(address REGEXP '%s' OR name REGEXP '%s')"
366
%( self.escape(regexp), self.escape(regexp) ) )
368
# new method to get faster results when querying the number of subscribers
369
def getMembersCount(self, reason=None):
371
where = " AND digest='%s'" %reason
374
self.query("SELECT COUNT(*) FROM `%s` WHERE %s%s" %(
375
self._table, self._where, where))
376
count = mm_cfg.cursor.fetchone()
379
# get member's name (slow method if you need many)
380
# due to the way escape() is built, names are stored in html
381
# format in the DB, hence the canonstr() to put them back to
383
def getMemberName(self, member):
384
name = self.select_on('name', member)
387
return Utils.canonstr(name[0])
390
self.__assertIsMember(member)
393
def getMemberTopics(self, member):
394
topics = self.select_on('topics_userinterest',member)
395
if len(topics) and isinstance(topics[0], str):
396
return topics[0].split(',')
398
self.__assertIsMember(member)
401
def getDeliveryStatus(self, member):
402
status = self.select_on('delivery_status',member)
404
if status[0] in (MemberAdaptor.ENABLED, MemberAdaptor.UNKNOWN,
405
MemberAdaptor.BYUSER, MemberAdaptor.BYADMIN,
406
MemberAdaptor.BYBOUNCE):
409
return MemberAdaptor.ENABLED
410
self.__assertIsMember(member)
413
# delivery status change time
414
def getDeliveryStatusChangeTime(self, member):
415
time = self.select_on('delivery_status_timestamp',member)
419
return MemberAdaptor.ENABLED
422
self.__assertIsMember(member)
424
# Covered by SQL getMembers(), and getDeliveryStatus().
425
def getDeliveryStatusMembers(self, status=(MemberAdaptor.UNKNOWN,
426
MemberAdaptor.BYUSER,
427
MemberAdaptor.BYADMIN,
428
MemberAdaptor.BYBOUNCE)):
429
return [member for member in self.getMembers()
430
if self.getDeliveryStatus(member) in status]
432
# show bouncing members
433
def getBouncingMembers(self):
434
self.query("""SELECT bi_cookie,bi_score,bi_noticesleft,
435
UNIX_TIMESTAMP(bi_lastnotice),UNIX_TIMESTAMP(bi_date),address
436
FROM `%s` WHERE %s""" %(self._table, self._where))
437
# get the number of rows in the resultset
438
numrows = int(mm_cfg.cursor.rowcount)
439
# save one address at a time
440
bounce_info_list = []
441
for x in range(0,numrows):
442
row = mm_cfg.cursor.fetchone()
443
# We must not return anything if there is
444
# no bounce info for that member to start with.
446
# Append the member name to the bounce info list.
447
bounce_info_list.append(row[5])
448
return [member.lower() for member in bounce_info_list]
450
def getBounceInfo(self, member):
455
MONTH(bi_lastnotice),
456
DAYOFMONTH(bi_lastnotice),
461
FROM `%s` WHERE %s AND """ %(self._table, self._where)
462
+ ("address = '%s'" %( self.escape(member) ) ))
463
numrows = int(mm_cfg.cursor.rowcount)
465
self.__assertIsMember(member)
466
row = mm_cfg.cursor.fetchone()
470
# Otherwise, populate a bounce_info structure.
471
bounce_info = _BounceInfo(member, row[0],
472
(row[5],row[6],row[7]), row[1])
473
bounce_info.lastnotice = (row[2],row[3],row[4])
474
bounce_info.cookie = row[8]
481
def addNewMember(self, member, **kws):
482
# assert self.__mlist.Locked()
483
# Make sure this address isn't already a member
484
if self.isMember(member):
485
raise Errors.MMAlreadyAMember, member
488
password = Utils.MakeRandomPassword()
489
language = self.__mlist.preferred_language
491
if kws.has_key('digest'):
492
digest = kws['digest']
494
if kws.has_key('password'):
495
password = kws['password']
497
if kws.has_key('language'):
498
language = kws['language']
500
if kws.has_key('realname'):
501
realname = kws['realname']
503
# Assert that no other keywords are present
505
raise ValueError, kws.keys()
506
# If the localpart has uppercase letters in it, then the value in the
507
# members (or digest_members) dict is the case preserved address.
508
# Otherwise the value is 0. Note that the case of the domain part is
510
if Utils.LCDomain(member) == member.lower():
514
member = member.lower()
519
# All we need to do here is add the address.
520
# and Set the member's default set of options
521
if self.__mlist.new_member_options:
522
options = self.__mlist.new_member_options
525
if self.getTableType() is 'flat':
526
query = "INSERT INTO `%s` " %(self._table) \
527
+ "(listname, address, user_options, password, lang, " \
528
+ "digest, delivery_status) values " \
529
+ "('%s','%s',%s,'%s','%s','%s','%s')"
531
query = "INSERT INTO `%s` " \
532
+ "(address, user_options, password, lang, " \
533
+ "digest, delivery_status) values " \
534
+ "('%s',%s,'%s','%s','%s','%s')"
535
query = query %( self.__mlist.internal_name(),
536
self.escape(member), options, password,
537
language, digest, MemberAdaptor.ENABLED)
538
if mm_cfg.PGSQL_MEMBER_DB_VERBOSE:
539
syslog('pgsql',query)
540
mm_cfg.cursor.execute(query)
542
self.setMemberName(member, realname)
544
def removeMember(self, member):
545
# assert self.__mlist.Locked()
546
self.__assertIsMember(member)
547
self.query("DELETE FROM `%s` WHERE %s " %(self._table, self._where)
548
+ ("AND address = '%s'" %( self.escape(member.lower()) ) ))
551
def changeMemberAddress(self, member, newaddress, nodelete=0):
552
# assert self.__mlist.Locked()
553
# Make sure this address isn't already a member
554
if self.isMember(newaddress):
555
raise Errors.MMAlreadyAMember, newaddress
556
self.update_on('address', newaddress, member)
558
def setMemberPassword(self, member, password):
559
# assert self.__mlist.Locked()
560
self.update_on('password', password, member)
562
def setMemberLanguage(self, member, lang):
563
# assert self.__mlist.Locked()
564
self.update_on('lang', lang, member)
566
def setMemberOption(self, member, flag, value):
567
# assert self.__mlist.Locked()
568
if flag == mm_cfg.Digests:
570
# Be sure the list supports digest delivery
571
if not self.__mlist.digestable:
572
raise Errors.CantDigestError
573
# The user is turning on digest mode
574
# If they are already receiving digests, report an error.
575
if self.getMemberOption(member, mm_cfg.Digests) is 'Y':
576
raise Errors.AlreadyReceivingDigests, member
577
# If we've got past all this, actually turn on digest mode.
578
self.update_on('digest', 'Y', member)
580
# Be sure the list supports regular delivery
581
if not self.__mlist.nondigestable:
582
raise Errors.MustDigestError
583
# The user is turning off digest mode
584
# If they are already receiving regular, report an error.
585
if self.getMemberOption(member, mm_cfg.Digests) is 'N':
586
raise Errors.AlreadyReceivingRegularDeliveries, member
587
# If we've got past all this, actually turn off digest mode.
588
self.update_on('digest', 'N', member)
592
self.query("UPDATE `%s` " %(self._table)
593
+ ("SET user_options = user_options | %s " %(flag))
594
+ "WHERE %s " %(self._where)
595
+ ("AND address = '%s'" %( self.escape(member) ) ))
597
self.query("UPDATE `%s` " %(self._table)
598
+ ("SET user_options = user_options & ~%s " %(flag))
599
+ "WHERE %s " %(self._where)
600
+ ("AND address = '%s'" %( self.escape(member) ) ))
604
def setMemberName(self, member, name):
605
# assert self.__mlist.Locked()
606
self.update_on('name', name, member)
608
def setMemberTopics(self, member, topics):
609
# assert self.__mlist.Locked()
610
if isinstance(topics,list):
611
topics=",".join(topics)
614
self.query("UPDATE `%s` " %(self._table)
615
+ ("SET topics_userinterest = '%s' " %(
616
self.escape(topics) ))
617
+ "WHERE %s " %(self._where)
618
+ ("AND address = '%s'" %( self.escape(member) )))
620
def setDeliveryStatus(self, member, status):
621
assert status in (MemberAdaptor.ENABLED, MemberAdaptor.UNKNOWN,
622
MemberAdaptor.BYUSER, MemberAdaptor.BYADMIN,
623
MemberAdaptor.BYBOUNCE)
624
# assert self.__mlist.Locked()
625
member = member.lower()
626
if status == MemberAdaptor.ENABLED:
627
# Enable by resetting their bounce info.
628
self.setBounceInfo(member, None)
630
self.query("UPDATE `%s` " %(self._table)
631
+ ("SET delivery_status = '%s', " %(status))
632
+ "delivery_status_timestamp=NOW() WHERE %s " %(self._where)
633
+ ("AND address = '%s'" %( self.escape(member) )))
637
def setBounceInfo(self, member, info):
638
# assert self.__mlist.Locked()
639
member = member.lower()
641
self.query("UPDATE `%s` " %(self._table)
642
+ ("SET delivery_status = '%s', " %(MemberAdaptor.ENABLED))
643
+ "bi_cookie = NULL, "
645
+ "bi_noticesleft = 0, "
646
+ "bi_lastnotice = '0000-00-00', "
647
+ "bi_date = '0000-00-00' "
648
+ "WHERE %s " %(self._where)
649
+ ("AND address = '%s'" %( self.escape(member) )))
651
lnsql = time.strftime("%Y-%m-%d", time.strptime('-'.join(map(str,info.lastnotice)),'%Y-%m-%d'))
652
datesql = time.strftime("%Y-%m-%d", time.strptime('-'.join(map(str,info.date)),'%Y-%m-%d'))
653
self.query("UPDATE `%s` " %(self._table)
654
+ (("SET bi_cookie = '%s', "
656
+ "bi_noticesleft = %s, "
657
+ "bi_lastnotice = '%s', "
658
+ "bi_date = '%s' ") %(
659
info.cookie, info.score,
660
info.noticesleft, lnsql, datesql
662
+ ("WHERE %s " %(self._where))
663
+ ("AND address = '%s'" %( self.escape(member) )))
667
# this function can be plugged into Mailman.MailList.Save
668
# it saves a copy of a few list's attributes into a database
669
def SaveToDb(self,dict):
670
query = 'REPLACE lists (listname,moderation,advertised,new_member_options,subscribe_policy,host_name,description,info,count) ' \
671
" VALUES ('%s','%s','%s','%s','%s','%s','%s','%s','%s')" % (\
672
self.internal_name(), \
673
self.default_member_moderation, \
675
self.new_member_options, \
676
self.subscribe_policy, \
678
self.escape(self.description), \
679
self.escape(self.info), \
680
self.getMembersCount() \
682
syslog('pgsql', query)
686
syslog('pgsql', 'error %s'%e)