Port to Trac 0.12.x. Added mailarchive subcommands of trac-admin instread of TracMailArchive-admin command.
@@ -0,0 +1,518 @@ | ||
1 | +# -*- coding: utf-8 -*- | |
2 | +# MailArchive plugin | |
3 | + | |
4 | +import calendar | |
5 | +import email | |
6 | +import email.Errors | |
7 | +import email.Utils | |
8 | +import mailbox | |
9 | +import mimetypes | |
10 | +import os | |
11 | +import poplib | |
12 | +import re | |
13 | +import time | |
14 | +import traceback | |
15 | +from email.Utils import unquote | |
16 | + | |
17 | +from trac.core import Component, implements | |
18 | +from trac.admin.api import IAdminCommandProvider | |
19 | +from trac.util import NaivePopen | |
20 | +from trac.attachment import Attachment | |
21 | + | |
22 | + | |
23 | +class MailArchiveAdmin(Component): | |
24 | + implements(IAdminCommandProvider) | |
25 | + | |
26 | + def get_admin_commands(self): | |
27 | + yield ('mailarchive import', '<mlname> <filepath>', | |
28 | + 'import UnixMail', | |
29 | + None, self._do_import) | |
30 | + yield ('mailarchive pop3', '<mlname>', | |
31 | + 'import from pop3 server', | |
32 | + None, self._do_pop3) | |
33 | + | |
34 | + | |
35 | + def all_docs(cls): | |
36 | + return (cls._help_help) | |
37 | + all_docs = classmethod(all_docs) | |
38 | + | |
39 | + | |
40 | + | |
41 | + def msgfactory(self,fp): | |
42 | + try: | |
43 | + return email.message_from_file(fp) | |
44 | + except email.Errors.MessageParseError: | |
45 | + # Don't return None since that will | |
46 | + # stop the mailbox iterator | |
47 | + return '' | |
48 | + | |
49 | + def decode_to_unicode(self, basestr): | |
50 | + # http://www.python.jp/pipermail/python-ml-jp/2004-June/002932.html | |
51 | + # Make mail header string to unicode string | |
52 | + | |
53 | + decodefrag = email.Header.decode_header(basestr) | |
54 | + subj_fragments = ['',] | |
55 | + for frag, enc in decodefrag: | |
56 | + if enc: | |
57 | + frag = self.to_unicode(frag, enc) | |
58 | + subj_fragments.append(frag) | |
59 | + return ''.join(subj_fragments) | |
60 | + | |
61 | + def to_unicode(self,text,charset): | |
62 | + if text=='': | |
63 | + return '' | |
64 | + | |
65 | + default_charset = self.env.config.get('mailarchive', 'default_charset',None) | |
66 | + if default_charset : | |
67 | + charset = default_charset | |
68 | + | |
69 | + # to unicode with codecaliases | |
70 | + # codecaliases change mail charset to python charset | |
71 | + charset = charset.lower( ) | |
72 | + aliases = {} | |
73 | + aliases_text = self.env.config.get('mailarchive', 'codecaliases') | |
74 | + for alias in aliases_text.split(','): | |
75 | + alias_s = alias.split(':') | |
76 | + if len(alias_s) >=2: | |
77 | + if alias_s[1] == 'cmd': | |
78 | + aliases[alias_s[0].lower()] = ('cmd',alias_s[2]) | |
79 | + else: | |
80 | + aliases[alias_s[0].lower()] = ('codec',alias_s[1]) | |
81 | + | |
82 | + if aliases.has_key(charset): | |
83 | + (type,alias) = aliases[charset] | |
84 | + if type == 'codec': | |
85 | + text = unicode(text,alias) | |
86 | + elif type == 'cmd': | |
87 | + np = NaivePopen(alias, text, capturestderr=1) | |
88 | + if np.errorlevel or np.err: | |
89 | + err = 'Failed: %s, %s.' % (np.errorlevel, np.err) | |
90 | + raise Exception, err | |
91 | + text = unicode(np.out,'utf-8') | |
92 | + else: | |
93 | + text = unicode(text,charset) | |
94 | + return text | |
95 | + | |
96 | + def import_message(self, msg, author,mlid, db): | |
97 | + OUTPUT_ENCODING = 'utf-8' | |
98 | + subject = '' | |
99 | + messageid = '' | |
100 | + utcdate = 0 | |
101 | + localdate = 0 | |
102 | + zoneoffset = 0 | |
103 | + text = '' | |
104 | + body = '' | |
105 | + ref_messageid = '' | |
106 | + | |
107 | + cursor = db.cursor() | |
108 | + is_newid = False | |
109 | + | |
110 | + if 'message-id' in msg: | |
111 | + messageid = msg['message-id'] | |
112 | + if messageid[:1] == '<': | |
113 | + messageid = messageid[1:] | |
114 | + if messageid[-1:] == '>': | |
115 | + messageid = messageid[:-1] | |
116 | + self.print_debug('Message-ID:%s' % messageid ) | |
117 | + | |
118 | + #check messageid is unique | |
119 | + self.print_debug("Creating new mailarc '%s'" % 'mailarc') | |
120 | + cursor.execute("SELECT id from mailarc WHERE messageid=%s",(messageid,)) | |
121 | + row = cursor.fetchone() | |
122 | + id = None | |
123 | + if row: | |
124 | + id = row[0] | |
125 | + if id == None or id == "": | |
126 | + # why? get_last_id return 0 at first. | |
127 | + #id = db.get_last_id(cursor, 'mailarc') | |
128 | + is_newid = True | |
129 | + cursor.execute("SELECT Max(id)+1 as id from mailarc") | |
130 | + row = cursor.fetchone() | |
131 | + if row and row[0] != None: | |
132 | + id = row[0] | |
133 | + else: | |
134 | + id = 1 | |
135 | + id = int(id) # Because id might be 'n.0', int() is called. | |
136 | + | |
137 | + | |
138 | + if 'date' in msg: | |
139 | + datetuple_tz = email.Utils.parsedate_tz(msg['date']) | |
140 | + localdate = calendar.timegm(datetuple_tz[:9]) #toDB | |
141 | + zoneoffset = datetuple_tz[9] # toDB | |
142 | + utcdate = localdate-zoneoffset # toDB | |
143 | + #make zone ( +HHMM or -HHMM | |
144 | + zone = '' | |
145 | + if zoneoffset >0: | |
146 | + zone = '+' + time.strftime('%H%M',time.gmtime(zoneoffset)) | |
147 | + elif zoneoffset < 0: | |
148 | + zone = '-' + time.strftime('%H%M',time.gmtime(-1*zoneoffset)) | |
149 | + | |
150 | + #self.print_debug( time.strftime("%y/%m/%d %H:%M:%S %z",datetuple_tz[:9])) | |
151 | + self.print_debug( time.strftime("%Y/%m/%d %H:%M:%S",time.gmtime(utcdate))) | |
152 | + self.print_debug( time.strftime("%Y/%m/%d %H:%M:%S",time.gmtime(localdate))) | |
153 | + self.print_debug(zone) | |
154 | + | |
155 | + fromname,fromaddr = email.Utils.parseaddr(msg['from']) | |
156 | + fromname = self.decode_to_unicode(fromname) | |
157 | + fromaddr = self.decode_to_unicode(fromaddr) | |
158 | + | |
159 | + self.print_info( ' ' + time.strftime("%Y/%m/%d %H:%M:%S",time.gmtime(localdate))+' ' + zone +' '+ fromaddr) | |
160 | + | |
161 | + if 'subject' in msg: | |
162 | + subject = self.decode_to_unicode(msg['subject']) | |
163 | + self.print_debug( subject.encode(OUTPUT_ENCODING)) | |
164 | + | |
165 | + # make thread infomations | |
166 | + ref_messageid = '' | |
167 | + if 'in-reply-to' in msg: | |
168 | + ref_messageid = ref_messageid + msg['In-Reply-To'] + ' ' | |
169 | + self.print_debug('In-Reply-To:%s' % ref_messageid ) | |
170 | + | |
171 | + if 'references' in msg: | |
172 | + ref_messageid = ref_messageid + msg['References'] + ' ' | |
173 | + | |
174 | + m = re.findall(r'<(.+?)>', ref_messageid) | |
175 | + ref_messageid = '' | |
176 | + for text in m: | |
177 | + ref_messageid = ref_messageid + "'%s'," % text | |
178 | + ref_messageid = ref_messageid.strip(',') | |
179 | + self.print_debug('RefMessage-ID:%s' % ref_messageid ) | |
180 | + | |
181 | + | |
182 | + # multipart mail | |
183 | + if msg.is_multipart(): | |
184 | + body = '' | |
185 | + # delete all attachement at message-id | |
186 | + Attachment.delete_all(self.env, 'mailarchive', id, db) | |
187 | + | |
188 | + for part in msg.walk(): | |
189 | + content_type = part.get_content_type() | |
190 | + self.print_debug('Content-Type:'+content_type) | |
191 | + file_counter = 1 | |
192 | + | |
193 | + if content_type == 'multipart/mixed': | |
194 | + pass | |
195 | + elif content_type == 'text/html' and self.is_file(part) == False: | |
196 | + body = part.get_payload(decode=1) | |
197 | + elif content_type == 'text/plain' and self.is_file(part) == False: | |
198 | + body = part.get_payload(decode=1) | |
199 | + charset = part.get_content_charset() | |
200 | + self.print_debug('charset:'+str(charset)) | |
201 | + # Todo:need try | |
202 | + if charset != None: | |
203 | + body = self.to_unicode(body,charset) | |
204 | + elif part.get_payload(decode=1) == None: | |
205 | + pass | |
206 | + else: | |
207 | + self.print_debug( part.get_content_type()) | |
208 | + # get filename | |
209 | + # Applications should really sanitize the given filename so that an | |
210 | + # email message can't be used to overwrite important files | |
211 | + filename = self.get_filename(part) | |
212 | + if not filename: | |
213 | + ext = mimetypes.guess_extension(part.get_content_type()) | |
214 | + if not ext: | |
215 | + # Use a generic bag-of-bits extension | |
216 | + ext = '.bin' | |
217 | + filename = 'part-%03d%s' % (file_counter, ext) | |
218 | + file_counter += 1 | |
219 | + | |
220 | + self.print_debug("filename:" + filename.encode(OUTPUT_ENCODING)) | |
221 | + | |
222 | + # make attachment | |
223 | + tmp = os.tmpfile() | |
224 | + tempsize =len(part.get_payload(decode=1)) | |
225 | + tmp.write(part.get_payload(decode=1)) | |
226 | + | |
227 | + tmp.flush() | |
228 | + tmp.seek(0,0) | |
229 | + | |
230 | + attachment = Attachment(self.env,'mailarchive', id) | |
231 | + | |
232 | + attachment.description = '' # req.args.get('description', '') | |
233 | + attachment.author = author #req.args.get('author', '') | |
234 | + attachment.ipnr = '127.0.0.1' | |
235 | + | |
236 | + try: | |
237 | + attachment.insert(filename, | |
238 | + tmp, tempsize,None,db) | |
239 | + except Exception, e: | |
240 | + try: | |
241 | + ext = filename.split('.')[-1] | |
242 | + if ext == filename: | |
243 | + ext = '.bin' | |
244 | + else: | |
245 | + ext = '.' + ext | |
246 | + filename = 'part-%03d%s' % (file_counter, ext) | |
247 | + file_counter += 1 | |
248 | + attachment.insert(filename, | |
249 | + tmp, tempsize,None,db) | |
250 | + self.print_warning('As name is too long, the attached file is renamed : '+filename) | |
251 | + | |
252 | + except Exception, e: | |
253 | + self.print_error('Exception at attach file of Message-ID:'+messageid) | |
254 | + self.print_error( e ) | |
255 | + | |
256 | + tmp.close() | |
257 | + | |
258 | + # not multipart mail | |
259 | + else: | |
260 | + # Todo:if Content-Type = text/html then convert htmlMail to text | |
261 | + content_type = msg.get_content_type() | |
262 | + self.print_debug('Content-Type:'+content_type) | |
263 | + if content_type == 'text/html': | |
264 | + body = 'html' | |
265 | + else: | |
266 | + #body | |
267 | + #self.print_debug(msg.get_content_type()) | |
268 | + body = msg.get_payload(decode=1) | |
269 | + charset = msg.get_content_charset() | |
270 | + | |
271 | + # need try: | |
272 | + if charset != None: | |
273 | + self.print_debug("charset:"+charset) | |
274 | + body = self.to_unicode(body,charset) | |
275 | + | |
276 | + | |
277 | + #body = body.replace(os.linesep,'\n') | |
278 | + self.print_debug('Thread') | |
279 | + | |
280 | + thread_parent = ref_messageid.replace("'",'').replace(',',' ') | |
281 | + thread_root = '' | |
282 | + if thread_parent !='': | |
283 | + # sarch first parent id | |
284 | + self.print_debug("SearchThread;"+thread_parent) | |
285 | + cursor = db.cursor() | |
286 | + sql = "SELECT threadroot,messageid FROM mailarc where messageid in (%s)" % ref_messageid | |
287 | + self.print_debug(sql) | |
288 | + cursor.execute(sql) | |
289 | + | |
290 | + row = cursor.fetchone() | |
291 | + if row: | |
292 | + #thread_parent = row[1] | |
293 | + if row[0] == '': | |
294 | + thread_root = thread_parent.split(' ').pop() | |
295 | + self.print_debug("AddToThread;"+thread_root) | |
296 | + else: | |
297 | + thread_root = row[0] | |
298 | + self.print_debug("NewThread;"+thread_root) | |
299 | + else: | |
300 | + self.print_debug("NoThread;"+thread_parent) | |
301 | + thread_root = thread_root.strip() | |
302 | + | |
303 | + self.print_debug('Insert') | |
304 | + | |
305 | + if messageid != '': | |
306 | + | |
307 | + # insert or update mailarc_category | |
308 | + | |
309 | + yearmonth = time.strftime("%Y%m",time.gmtime(utcdate)) | |
310 | + category = mlid+yearmonth | |
311 | + cursor.execute("SELECT category,mlid,yearmonth,count FROM mailarc_category WHERE category=%s", | |
312 | + (category,)) | |
313 | + row = cursor.fetchone() | |
314 | + count = 0 | |
315 | + if row: | |
316 | + count = row[3] | |
317 | + pass | |
318 | + else: | |
319 | + cursor.execute("INSERT INTO mailarc_category (category,mlid,yearmonth,count) VALUES(%s,%s,%s,%s)", | |
320 | + (category, mlid, yearmonth, 0)) | |
321 | + if is_newid == True: | |
322 | + count = count +1 | |
323 | + cursor.execute("UPDATE mailarc_category SET count=%s WHERE category=%s" , | |
324 | + (count, category)) | |
325 | + | |
326 | + # insert or update mailarc | |
327 | + | |
328 | + #self.print_debug( | |
329 | + # "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)" %(str(id), | |
330 | + # category.encode('utf-8'), | |
331 | + # messageid, | |
332 | + # utcdate, | |
333 | + # zoneoffset, | |
334 | + # subject.encode('utf-8'), fromname.encode('utf-8'), | |
335 | + # fromaddr.encode('utf-8'),'','', | |
336 | + # thread_root,thread_parent)) | |
337 | + cursor.execute("DELETE FROM mailarc where messageid=%s",(messageid,)) | |
338 | + cursor.execute("INSERT INTO mailarc (" | |
339 | + "id,category,messageid,utcdate,zoneoffset,subject," | |
340 | + "fromname,fromaddr,header,text, threadroot,threadparent ) " | |
341 | + "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", | |
342 | + (id, category, messageid, utcdate, zoneoffset, subject, | |
343 | + fromname, fromaddr,'',body, thread_root,thread_parent)) | |
344 | + | |
345 | + db.commit() | |
346 | + | |
347 | + def do_refresh_category(self,line): | |
348 | + db = self.db_open() | |
349 | + self.env = self.env_open() | |
350 | + cursor = db.cursor() | |
351 | + cursor.execute("DELETE FROM mailarc_category") | |
352 | + cursor.execute("SELECT category, count(*) as cnt from mailarc GROUP BY category ") | |
353 | + for category,cnt in cursor: | |
354 | + cursor2 = db.cursor() | |
355 | + cursor2.execute("INSERT INTO mailarc_category (category,mlid,yearmonth,count) VALUES(%s,%s,%s,%s)",(category,category[:-6],category[-6:],cnt)) | |
356 | + db.commit() | |
357 | + | |
358 | + def _do_import(self, mlname, filepath): | |
359 | + @self.env.with_transaction() | |
360 | + def do_import(db): | |
361 | + self._import_unixmailbox('cmd', db, mlname, filepath) | |
362 | + | |
363 | + def _do_pop3(self, mlname): | |
364 | + @self.env.with_transaction() | |
365 | + def do_pop3(db): | |
366 | + self._import_from_pop3('cmd', db, mlname) | |
367 | + | |
368 | + def print_info(self,line): | |
369 | + print "%s" % line | |
370 | + | |
371 | + def print_debug(self,line): | |
372 | + #print "[Debug] %s" % line | |
373 | + pass | |
374 | + | |
375 | + def print_error(self,line): | |
376 | + print "[Error] %s" % line | |
377 | + | |
378 | + def print_warning(self,line): | |
379 | + print "[Warning] %s" % line | |
380 | + | |
381 | + def _import_unixmailbox(self,author, db, mlid, msgfile_path): | |
382 | + self.print_debug('import_mail') | |
383 | + | |
384 | + #paser = Parser() | |
385 | + | |
386 | + self.print_info("%s Start Importing %s ..." % | |
387 | + (time.strftime("%Y/%m/%d %H:%M:%S",time.gmtime()),msgfile_path)) | |
388 | + | |
389 | + fp = None | |
390 | + try: | |
391 | + fp = open(msgfile_path,"rb") | |
392 | + mbox = mailbox.UnixMailbox(fp, self.msgfactory) | |
393 | + | |
394 | + counter =1 | |
395 | + msg = mbox.next() | |
396 | + while msg is not None: | |
397 | + messageid = '' | |
398 | + try: | |
399 | + messageid = msg['message-id'] | |
400 | + self.import_message(msg,author,mlid,db) | |
401 | + except Exception, e: | |
402 | + self.print_error('Exception At Message-ID: %r' % (messageid,)) | |
403 | + self.print_error( e ) | |
404 | + traceback.print_exc() | |
405 | + | |
406 | + if counter > 10000: | |
407 | + break | |
408 | + msg = mbox.next() | |
409 | + counter = counter + 1 | |
410 | + finally: | |
411 | + fp.close() | |
412 | + | |
413 | + self.print_info("End Imporing %s. " % msgfile_path) | |
414 | + | |
415 | + def _import_from_pop3(self,author, db, mlid): | |
416 | + | |
417 | + pop_server = self.env.config.get('mailarchive', 'pop3_server') | |
418 | + pop_user = self.env.config.get('mailarchive', 'pop3_user') | |
419 | + pop_password = self.env.config.get('mailarchive', 'pop3_password') | |
420 | + pop_delete = self.env.config.get('mailarchive', 'pop3_delete','none') | |
421 | + | |
422 | + if pop_server =='': | |
423 | + self.print_error('trac.ini mailarchive pop3_server is null!') | |
424 | + elif pop_user == '': | |
425 | + self.print_error('trac.ini mailarchive pop3_user is null!') | |
426 | + elif pop_password == '': | |
427 | + self.print_error('trac.ini mailarchive pop3_password is null!') | |
428 | + | |
429 | + self.print_info("%s Start Connction pop3 %s:%s ..." % | |
430 | + (time.strftime("%Y/%m/%d %H:%M:%S",time.gmtime()), | |
431 | + pop_server,pop_user)) | |
432 | + | |
433 | + pop = poplib.POP3(pop_server) | |
434 | + pop.user(pop_user) | |
435 | + pop.pass_(pop_password) | |
436 | + num_messages = len(pop.list()[1]) | |
437 | + counter = 1 | |
438 | + for i in range(num_messages): | |
439 | + #lines = ['',] | |
440 | + #for j in pop.retr(i+1)[1]: | |
441 | + # lines.append(j + os.linesep) | |
442 | + #mes_text = ''.join(lines) | |
443 | + mes_text = ''.join(['%s\n' % line for line in pop.retr(i+1)[1]]) | |
444 | + messageid = '' | |
445 | + exception_flag = False | |
446 | + try: | |
447 | + msg = email.message_from_string(mes_text) | |
448 | + messageid = msg['message-id'] | |
449 | + self.import_message(msg,author,mlid,db) | |
450 | + except Exception, e: | |
451 | + exception_flag = True | |
452 | + self.print_error('Exception At Message-ID:'+messageid) | |
453 | + self.print_error( e ) | |
454 | + | |
455 | + #if exception_flag == False: | |
456 | + # self.print_info(" Import Message Success") | |
457 | + | |
458 | + | |
459 | + # delete mail | |
460 | + if pop_delete == 'all': | |
461 | + pop.dele(i+1) | |
462 | + self.print_info(" Delete MailServer Message ") | |
463 | + elif pop_delete == 'imported': | |
464 | + if exception_flag == False: | |
465 | + pop.dele(i+1) | |
466 | + self.print_info(" Delete MailServer Message ") | |
467 | + else: | |
468 | + pass | |
469 | + | |
470 | + if counter > 10000: | |
471 | + break | |
472 | + counter = counter + 1 | |
473 | + | |
474 | + pop.quit() | |
475 | + | |
476 | + #if handle_ta: | |
477 | + db.commit() | |
478 | + self.print_info("End Reciving. " ) | |
479 | + | |
480 | + def is_file(self,part ): | |
481 | + """Return True:filename associated with the payload if present. | |
482 | + """ | |
483 | + missing = object() | |
484 | + filename = part.get_param('filename', missing, 'content-disposition') | |
485 | + if filename is missing: | |
486 | + filename = part.get_param('name', missing, 'content-disposition') | |
487 | + if filename is missing: | |
488 | + return False | |
489 | + return True | |
490 | + | |
491 | + def get_filename(self,part , failobj=None): | |
492 | + """Return the filename associated with the payload if present. | |
493 | + | |
494 | + The filename is extracted from the Content-Disposition header's | |
495 | + `filename' parameter, and it is unquoted. If that header is missing | |
496 | + the `filename' parameter, this method falls back to looking for the | |
497 | + `name' parameter. | |
498 | + """ | |
499 | + missing = object() | |
500 | + filename = part.get_param('filename', missing, 'content-disposition') | |
501 | + if filename is missing: | |
502 | + filename = part.get_param('name', missing, 'content-disposition') | |
503 | + if filename is missing: | |
504 | + return failobj | |
505 | + | |
506 | + errors='replace' | |
507 | + fallback_charset='us-ascii' | |
508 | + if isinstance(filename, tuple): | |
509 | + rawval = unquote(filename[2]) | |
510 | + charset = filename[0] or 'us-ascii' | |
511 | + try: | |
512 | + return self.to_unicode(rawval, charset) | |
513 | + except LookupError: | |
514 | + # XXX charset is unknown to Python. | |
515 | + return unicode(rawval, fallback_charset, errors) | |
516 | + else: | |
517 | + return self.decode_to_unicode(unquote(filename)) | |
518 | + |
@@ -0,0 +1,618 @@ | ||
1 | +# -*- coding: utf-8 -*- | |
2 | +# MailArchive plugin | |
3 | + | |
4 | +from datetime import datetime | |
5 | +import urllib | |
6 | +import time | |
7 | +import calendar | |
8 | +import re | |
9 | +import os | |
10 | +import tempfile | |
11 | +import email.Errors | |
12 | +import email.Utils | |
13 | +import mailbox | |
14 | +import mimetypes | |
15 | +import email | |
16 | +#from email.Parser import Parser | |
17 | +from email.Header import decode_header | |
18 | +#from email.Utils import collapse_rfc2231_value | |
19 | + | |
20 | +#011 | |
21 | +import pkg_resources | |
22 | + | |
23 | +from genshi.builder import tag | |
24 | + | |
25 | + | |
26 | +from trac.core import * | |
27 | +from trac.env import IEnvironmentSetupParticipant | |
28 | +#from trac.Search import ISearchSource, search_to_sql, shorten_result | |
29 | +from trac.search import ISearchSource, search_to_sql, shorten_result | |
30 | + | |
31 | +from trac.web import IRequestHandler | |
32 | +from trac.util import NaivePopen | |
33 | +from StringIO import StringIO | |
34 | + | |
35 | + | |
36 | + | |
37 | +from trac.wiki import wiki_to_html,wiki_to_oneliner, IWikiSyntaxProvider | |
38 | +from trac.util.html import html, Markup #0.10 | |
39 | +from trac.web.chrome import add_link, add_stylesheet,add_ctxtnav, prevnext_nav, \ | |
40 | + INavigationContributor, ITemplateProvider | |
41 | + | |
42 | +#0.11 from trac.attachment import attachment_to_hdf, attachments_to_hdf, Attachment, AttachmentModule | |
43 | +from trac.attachment import AttachmentModule | |
44 | + | |
45 | + | |
46 | +#011 from trac.Timeline import ITimelineEventProvider #same | |
47 | +from trac.timeline.api import ITimelineEventProvider | |
48 | + | |
49 | +#011 | |
50 | +from trac.util.translation import _ | |
51 | +from trac.resource import * | |
52 | +from trac.mimeview.api import Context | |
53 | + | |
54 | +from trac.mimeview import * | |
55 | +#from trac.mimeview.api import Mimeview, IContentConverter #0.10 | |
56 | + | |
57 | +#011 | |
58 | +#from trac.perm import PermissionError, PermissionSystem, IPermissionPolicy | |
59 | +from trac.perm import IPermissionRequestor | |
60 | +from trac.resource import IResourceManager | |
61 | +from trac.attachment import ILegacyAttachmentPolicyDelegate | |
62 | + | |
63 | +#011 | |
64 | +from trac.util.datefmt import to_timestamp, utc | |
65 | + | |
66 | +from trac.util.presentation import Paginator | |
67 | + | |
68 | +def get_author(fromname,fromaddr): | |
69 | + author = fromname | |
70 | + if fromname=='': | |
71 | + if re.match('(.+?)@',fromaddr): | |
72 | + author = re.match('(.+?)@',fromaddr).group(1) | |
73 | + if author == None or author.strip() =='': | |
74 | + author = '--' | |
75 | + return author | |
76 | + | |
77 | +class Timeline(Component): | |
78 | + implements(ITimelineEventProvider) | |
79 | + | |
80 | + # ITimelineEventProvider methods | |
81 | + | |
82 | + def get_timeline_filters(self, req): | |
83 | + if 'MAILARCHIVE_VIEW' in req.perm: | |
84 | + yield ('mailarchive', _(self.env.config.get('mailarchive', 'title','MailArchive'))) | |
85 | + | |
86 | + def get_timeline_events(self, req, start, stop, filters): | |
87 | + if 'mailarchive' in filters: | |
88 | + add_stylesheet(req, 'mailarchive/css/mailarchive.css') | |
89 | + | |
90 | + db = self.env.get_db_cnx() | |
91 | + mailarchive_realm = Resource('mailarchive') | |
92 | + cursor = db.cursor() | |
93 | + | |
94 | + cursor.execute("SELECT id,category as mlname,utcdate as localdate," | |
95 | + "fromname,fromaddr , subject FROM mailarc " | |
96 | + "WHERE utcdate>=%s AND utcdate<=%s ", | |
97 | + (to_timestamp(start), to_timestamp(stop))) | |
98 | + for id,category,localdate, fromname, fromaddr,subject in cursor: | |
99 | + #if 'WIKI_VIEW' not in req.perm('wiki', name): | |
100 | + # continue | |
101 | + author = get_author(fromname,fromaddr) | |
102 | + #ctx = context('mailarchive', id) | |
103 | + | |
104 | + resource = mailarchive_realm(id=id,version=None) | |
105 | + if 'MAILARCHIVE_VIEW' not in req.perm(resource): | |
106 | + continue | |
107 | + yield ('mailarchive', | |
108 | + datetime.fromtimestamp(localdate, utc), | |
109 | + author or '--', | |
110 | + (resource,(category,author,subject))) | |
111 | + | |
112 | + | |
113 | + def render_timeline_event(self, context, field, event): | |
114 | + mailarchive_page,(category,author,subject) = event[3] | |
115 | + if field == 'url': | |
116 | + return context.href.mailarchive(mailarchive_page.id, version=mailarchive_page.version) | |
117 | + elif field == 'title': | |
118 | + markup = tag(u'メールが ',category,u'に送信されました') | |
119 | + return markup | |
120 | + elif field == 'description': | |
121 | + markup = tag(subject) | |
122 | + return markup | |
123 | + | |
124 | + | |
125 | +class SearchProvider(Component): | |
126 | + implements(ISearchSource) | |
127 | + | |
128 | + # ISearchProvider methods | |
129 | + | |
130 | + def get_search_filters(self, req): | |
131 | + if 'MAILARCHIVE_VIEW' in req.perm: | |
132 | + yield ('mailarchive', self.env.config.get('mailarchive', 'title','MailArchive')) | |
133 | + | |
134 | + | |
135 | + def get_search_results(self, req, terms, filters): | |
136 | + if 'mailarchive' in filters: | |
137 | + db = self.env.get_db_cnx() | |
138 | + sql_query, args = search_to_sql(db, ['m1.messageid','m1.subject','m1.fromname','m1.fromaddr','m1.text'],terms) | |
139 | + cursor = db.cursor() | |
140 | + cursor.execute("SELECT m1.id,m1.subject,m1.fromname,m1.fromaddr,m1.text,m1.utcdate as localdate " | |
141 | + "FROM mailarc m1 " | |
142 | + "WHERE " | |
143 | + "" + sql_query, args) | |
144 | + mailarchive_realm = Resource('mailarchive') | |
145 | + | |
146 | + for id,subject,fromname,fromaddr, text,localdate in cursor: | |
147 | + #if 'WIKI_VIEW' in req.perm('wiki', name): | |
148 | + resource = mailarchive_realm(id=id,version=None) | |
149 | + if 'MAILARCHIVE_VIEW' not in req.perm(resource): | |
150 | + continue | |
151 | + | |
152 | + yield (req.href.mailarchive(id), | |
153 | + subject, | |
154 | + datetime.fromtimestamp(localdate, utc), | |
155 | + get_author(fromname,fromaddr), | |
156 | + shorten_result(text, terms)) | |
157 | + | |
158 | + | |
159 | + | |
160 | +class MailArchiveModule(Component): | |
161 | + implements(ITemplateProvider, | |
162 | + IRequestHandler,IEnvironmentSetupParticipant,INavigationContributor, | |
163 | + IPermissionRequestor,ILegacyAttachmentPolicyDelegate,IResourceManager) | |
164 | + | |
165 | + # INavigationContributor methods | |
166 | + | |
167 | + def get_active_navigation_item(self, req): | |
168 | + return 'mailarchive' | |
169 | + | |
170 | + def get_navigation_items(self, req): | |
171 | + if 'MAILARCHIVE_VIEW' in req.perm('mailarchive'): | |
172 | + yield ('mainnav', 'mailarchive', | |
173 | + tag.a(_('MailArchive'), href=req.href.mailarchive())) | |
174 | + | |
175 | + # ITemplateProvider methods | |
176 | + | |
177 | + def get_htdocs_dirs(self): | |
178 | + return [('mailarchive',pkg_resources.resource_filename(__name__, 'htdocs'))] | |
179 | + | |
180 | + def get_templates_dirs(self): | |
181 | + return [pkg_resources.resource_filename(__name__, 'templates')] | |
182 | + | |
183 | + | |
184 | + # IRequestHandler methods | |
185 | + | |
186 | + def match_request(self, req): | |
187 | + match = re.match(r'^/mailarchive(?:/(.*))?', req.path_info) | |
188 | + if match: | |
189 | + if match.group(1): | |
190 | + req.args['messageid'] = match.group(1) | |
191 | + return 1 | |
192 | + | |
193 | + def process_request(self, req): | |
194 | + req.perm.assert_permission('MAILARCHIVE_VIEW') | |
195 | + db = self.env.get_db_cnx() | |
196 | + | |
197 | + messageid = req.args.get('messageid', '') | |
198 | + | |
199 | + #id = req.args.get('id','') | |
200 | + action = req.args.get('action', 'list') | |
201 | + | |
202 | + if action == 'import': | |
203 | + # brefore import lock db , in order to avoid import twice | |
204 | + self.import_unixmails(req.remote_addr,db) | |
205 | + # after import unlock db | |
206 | + return self._render_list(req, db, False,False) | |
207 | + | |
208 | + elif messageid != '': | |
209 | + return self._render_view(req, db, messageid) | |
210 | + else: | |
211 | + # did the user ask for any special report? | |
212 | + return self._render_list(req, db, False,False) | |
213 | + | |
214 | + # IEnvironmentSetupParticipant methods | |
215 | + | |
216 | + ienvironment_log = "" | |
217 | + def environment_created(self): | |
218 | + pass | |
219 | + | |
220 | + def environment_needs_upgrade(self, db): | |
221 | + cursor = db.cursor() | |
222 | + try: | |
223 | + cursor.execute("SELECT id FROM mailarc WHERE id='1'") | |
224 | + except : | |
225 | + db.rollback() | |
226 | + self.log.debug('MailArchive environment_needs_upgrade') | |
227 | + return True | |
228 | + | |
229 | + return False | |
230 | + | |
231 | + | |
232 | + def upgrade_environment(self, db): | |
233 | + self.log.debug('MailArchive upgrade_environment') | |
234 | + | |
235 | + sql = [ | |
236 | +""" | |
237 | +CREATE TABLE mailarc (id integer,category text, messageid text , | |
238 | + utcdate integer, zoneoffset integer, | |
239 | + subject text, fromname text, fromaddr text, header text, text text, | |
240 | + threadroot text, threadparent text); | |
241 | +""", | |
242 | +""" | |
243 | +CREATE TABLE mailarc_category ( category text,mlid text ,yearmonth text ,count integer); | |
244 | +""", | |
245 | +""" | |
246 | +CREATE UNIQUE INDEX mailarc_messageid_idx ON mailarc (messageid) | |
247 | +""", | |
248 | +""" | |
249 | +CREATE INDEX mailarc_id_idx ON mailarc (id) | |
250 | +""", | |
251 | +""" | |
252 | +CREATE INDEX mailarc_category_idx ON mailarc (category) | |
253 | +""", | |
254 | +""" | |
255 | +CREATE INDEX mailarc_utcdate_idx ON mailarc (utcdate) | |
256 | +""", | |
257 | +""" | |
258 | +CREATE UNIQUE INDEX mailarc_category_category_idx ON mailarc_category (category) | |
259 | +""", | |
260 | + ] | |
261 | + | |
262 | + | |
263 | + #db = self.env.get_db_cnx() | |
264 | + cursor = db.cursor() | |
265 | + for s in sql: | |
266 | + cursor.execute(s) | |
267 | + self.log.debug('%s' % s) | |
268 | + | |
269 | + #pass | |
270 | + | |
271 | + def _get_category_href(self,req,category): | |
272 | + return req.href.mailarchive()+'?category=%s' % category | |
273 | + def _get_href(self,req,id): | |
274 | + return req.href.mailarchive(id) | |
275 | + | |
276 | + def _render_view(self, req, db, id): | |
277 | + title, description, sql = ('ML名','MLの説明','select * from mailarc') | |
278 | + #req.hdf['mailarc.action'] = 'view' | |
279 | + data = {} | |
280 | + data['action'] = 'view' | |
281 | + | |
282 | + target_threadroot = '' | |
283 | + cursor = db.cursor() | |
284 | + cursor.execute("SELECT id,messageid,utcdate,zoneoffset,subject,fromname,fromaddr,text,threadroot FROM mailarc WHERE id=%s",(id,)) | |
285 | + | |
286 | + #messages = [] | |
287 | + for id,messageid,utcdate,zoneoffset,subject,fromname,fromaddr,text,threadroot in cursor: | |
288 | + prefix ='mailarc' | |
289 | + | |
290 | + #zone and date | |
291 | + zone = '' | |
292 | + if zoneoffset == '': | |
293 | + zoneoffset = 0 | |
294 | + if zoneoffset >0: | |
295 | + zone = ' +' + time.strftime('%H%M',time.gmtime(zoneoffset)) | |
296 | + elif zoneoffset < 0: | |
297 | + zone = ' -' + time.strftime('%H%M',time.gmtime(-1*zoneoffset)) | |
298 | + | |
299 | + localdate = time.strftime("%Y/%m/%d %H:%M:%S",time.gmtime(utcdate+zoneoffset)) | |
300 | + | |
301 | + # from | |
302 | + fromtext = '' | |
303 | + fromaddr = fromaddr.replace('@', | |
304 | + self.env.config.get('mailarchive', 'replaceat','@')) | |
305 | + #if fromname=='': | |
306 | + # fromtext = fromaddr | |
307 | + #else: | |
308 | + # fromtext = '%s (%s)' % (fromname,fromaddr) | |
309 | + fromtext = get_author(fromname,fromaddr) | |
310 | + | |
311 | + #subjectが空だとリンクにならない。 | |
312 | + if subject == None or subject.strip()=='': | |
313 | + subject = '___' | |
314 | + | |
315 | + message = { | |
316 | + 'id':id, | |
317 | + 'subject':subject, | |
318 | + 'href':req.href.mailarchive(id), | |
319 | + 'fromname':fromtext, | |
320 | + 'fromaddr':fromaddr, | |
321 | + 'senddate':localdate + zone, | |
322 | + 'messageid':messageid | |
323 | + } | |
324 | + target_threadroot = threadroot | |
325 | + | |
326 | + text = text.replace('@',self.env.config.get('mailarchive', 'replaceat','_at_') ) | |
327 | + | |
328 | + contentlines = text.splitlines() | |
329 | + htmllines = ['',] | |
330 | + for line in contentlines: | |
331 | + if self.env.config.get('mailarchive', 'wikiview','enabled') == 'enabled': | |
332 | + htmllines.append(wiki_to_oneliner(line, self.env,db,True,True,req)) | |
333 | + else: | |
334 | + htmllines.append(Markup(Markup().escape(line).replace(' ',' '))) | |
335 | + | |
336 | + content = Markup('<br/>').join(htmllines) | |
337 | + | |
338 | + message['page_html'] = content | |
339 | + #messages.append(message) | |
340 | + | |
341 | + break | |
342 | + | |
343 | + # Todo:Raise error when messsageid is wrong. | |
344 | + # List attached files | |
345 | + #req.perm.require('ATTACHMENT_VIEW') | |
346 | + context = Context.from_request(req, Resource('mailarchive', str(id), None)) | |
347 | + #self.log.debug(context) | |
348 | + data['attachments']=AttachmentModule(self.env).attachment_data(context) | |
349 | + #self.log.debug(data['attachments']) | |
350 | + #req.hdf['mailarc.attachments'] = attachments_to_hdf(self.env, req, db, | |
351 | + # 'mailarchive', id) | |
352 | + | |
353 | + | |
354 | + #if req.perm.has_permission('TICKET_APPEND'): | |
355 | + #req.hdf['mailarc.attach_href'] = self.env.href.attachment('mailarchive', | |
356 | + # id) | |
357 | + | |
358 | + if 'mailarc_mails' in req.session: | |
359 | + self.log.debug(req.session['mailarc_mails']) | |
360 | + mails = req.session['mailarc_mails'].split() | |
361 | + if str(id) in mails: | |
362 | + idx = mails.index(str(id)) | |
363 | + if idx > 0: | |
364 | + #add_ctxtnav(req, _('first'), req.href.mailarchive(mails[0])) | |
365 | + add_link(req, _('prev'), req.href.mailarchive(mails[idx - 1])) | |
366 | + if idx < len(mails) - 1: | |
367 | + add_link(req, _('next'), req.href.mailarchive(mails[idx + 1])) | |
368 | + #add_ctxtnav(req, _('last'), req.href.mailarchive(mails[-1])) | |
369 | + add_link(req, _('up'), req.session['mailarc_category_href']) | |
370 | + prevnext_nav(req,u'メール', u'リストに戻る') | |
371 | + | |
372 | + | |
373 | + #if target_threadroot == '': | |
374 | + # target_threadroot = messageid | |
375 | + | |
376 | + ref_count=0 | |
377 | + reflist = [] | |
378 | + cursor.execute("SELECT id,messageid,utcdate,zoneoffset,subject,fromname,fromaddr,text,threadroot FROM mailarc WHERE messageid=%s or threadroot=%s ORDER BY utcdate",(target_threadroot,target_threadroot)) | |
379 | + for ref_id,ref_messageid,utcdate,zoneoffset,subject,fromname,fromaddr,text,threadroot in cursor: | |
380 | + ref_count = ref_count +1 | |
381 | + #subjectが空だとリンクにならない。 | |
382 | + if subject == None or subject.strip()=='': | |
383 | + subject = '___' | |
384 | + | |
385 | + ref ={ | |
386 | + 'id':str(ref_id), | |
387 | + 'subject':subject[:20], | |
388 | + 'subject_alt':subject, | |
389 | + 'fromname':get_author(fromname,fromaddr), | |
390 | + 'date':time.strftime("%Y/%m/%d %H:%M:%S",time.gmtime(utcdate+zoneoffset)), | |
391 | + 'href':'' | |
392 | + } | |
393 | + if messageid == ref_messageid: | |
394 | + pass | |
395 | + else: | |
396 | + ref['href'] =req.href.mailarchive(ref_id) | |
397 | + reflist.append(ref) | |
398 | + | |
399 | + add_stylesheet(req, 'mailarchive/css/mailarchive.css') | |
400 | + data['reflist'] = reflist | |
401 | + data['message'] = message | |
402 | + | |
403 | + return 'maildetail.html', data, None | |
404 | + | |
405 | + def month_add(self,year,month,add_month): | |
406 | + month = month + add_month | |
407 | + while month >12 or month <1: | |
408 | + if month > 12: | |
409 | + month = month - 12 | |
410 | + year = year + 1 | |
411 | + else : | |
412 | + month = month + 12 | |
413 | + year = year - 1 | |
414 | + | |
415 | + # Internal methods | |
416 | + def _render_list(self, req, db, thread_flag , month): | |
417 | + target_category = req.args.get('category', '') | |
418 | + #month = req.args.get('month', '') | |
419 | + #this_month = time.strftime("%Y/%m/%d %H:%M:%S",time.gmtime(utcdate+zoneoffset)) | |
420 | + | |
421 | + data = {} | |
422 | + | |
423 | + ids = ['',] | |
424 | + | |
425 | + title, description, sql = ('ML名','MLの説明','select * from mailarc') | |
426 | + #req.hdf['mailarc.mode'] = 'list' | |
427 | + data['mode'] = 'list' | |
428 | + | |
429 | + mesid_prefix = {} | |
430 | + | |
431 | + cursor = db.cursor() | |
432 | + cursor.execute("SELECT category,mlid,yearmonth,count FROM mailarc_category ORDER BY mlid,yearmonth DESC") | |
433 | + | |
434 | + mls = [] | |
435 | + pre_mlid = '' | |
436 | + for category,mlid,yearmonth,count in cursor: | |
437 | + if target_category == '': | |
438 | + target_category = category | |
439 | + | |
440 | + category_item = { | |
441 | + 'id': mlid + yearmonth, | |
442 | + 'name': mlid, | |
443 | + 'year': yearmonth[:4], | |
444 | + 'month': yearmonth[4:], | |
445 | + 'count': str(count), | |
446 | + 'href': self._get_category_href(req,category) | |
447 | + } | |
448 | + if category == target_category: | |
449 | + data['name'] = mlid | |
450 | + data['year'] = yearmonth[:4] | |
451 | + data['month'] = yearmonth[4:] | |
452 | + category_item['href'] = "" | |
453 | + | |
454 | + if pre_mlid != mlid: | |
455 | + mls.append({'name':mlid,'yearmonths':[]}) | |
456 | + pre_mlid = mlid | |
457 | + mls[-1]['yearmonths'].append(category_item) | |
458 | + | |
459 | + attachments_list = {} | |
460 | + cursor.execute("SELECT DISTINCT attachment.id as id ,mailarc.id as id2, utcdate FROM mailarc,attachment WHERE mailarc.category=%s AND CAST('mailarc.id' as text) = attachment.id AND attachment.type='mailarchive' ORDER BY utcdate",(target_category.encode('utf-8'),)) | |
461 | + for id,id2,utcdate in cursor: | |
462 | + attachments_list[str(id)] = 1 | |
463 | + | |
464 | + thread_flag = True | |
465 | + cursor.execute("SELECT id,messageid,utcdate,zoneoffset,subject,fromname,fromaddr,threadparent,threadroot FROM mailarc WHERE category=%s ORDER BY utcdate",(target_category.encode('utf-8'),)) | |
466 | + | |
467 | + #pagelize | |
468 | + results = [] | |
469 | + for id,messageid,utcdate,zoneoffset,subject,fromname,fromaddr,thread_parent,thread_root in cursor: | |
470 | + results.append((id,messageid,utcdate,zoneoffset,subject,fromname,fromaddr,thread_parent,thread_root)) | |
471 | + pagelized = self._pagelize_list(req,results,data) | |
472 | + | |
473 | + #make message tree | |
474 | + root_message = { | |
475 | + 'children':[] | |
476 | + } | |
477 | + messageid_to_message = {'':root_message} | |
478 | + for id,messageid,utcdate,zoneoffset,subject,fromname,fromaddr,thread_parent,thread_root in pagelized.items: | |
479 | + #subjectが空だとリンクにならない。 | |
480 | + if subject == None or subject.strip()=='': | |
481 | + subject = '___' | |
482 | + | |
483 | + #date | |
484 | + if zoneoffset == '': | |
485 | + zoneoffset = 0 | |
486 | + localdate = time.strftime("%Y/%m/%d %H:%M:%S",time.gmtime(utcdate+zoneoffset)) | |
487 | + zone = '' | |
488 | + if zoneoffset >0: | |
489 | + zone = ' +' + time.strftime('%H%M',time.gmtime(zoneoffset)) | |
490 | + elif zoneoffset < 0: | |
491 | + zone = ' -' + time.strftime('%H%M',time.gmtime(-1*zoneoffset)) | |
492 | + | |
493 | + message = { | |
494 | + 'subject':subject, | |
495 | + 'mail_href':req.href.mailarchive(id), | |
496 | + 'fromname':fromname, | |
497 | + 'senddate':localdate + zone, | |
498 | + 'threadparent':thread_parent, | |
499 | + 'threadroot':thread_root, | |
500 | + 'attachment':0, | |
501 | + 'children':[] | |
502 | + } | |
503 | + if fromname=='' and re.match('(.+?)@',fromaddr): | |
504 | + message['fromname'] = re.match('(.+?)@',fromaddr).group(1) | |
505 | + | |
506 | + if attachments_list.has_key(str(id)) : | |
507 | + message['attachment'] = 1 | |
508 | + | |
509 | + #Search Parent | |
510 | + messages = self._serach_parent(messageid_to_message,thread_parent) | |
511 | + if messages.has_key('children'): | |
512 | + messages['children'].append(message) | |
513 | + #ソートを逆順にするにはappendでなくinsertを使うこと | |
514 | + messageid_to_message[messageid] = message | |
515 | + | |
516 | + ids.append(id) | |
517 | + | |
518 | + idstext = ''.join(['%s ' % id for id in ids]) | |
519 | + self.log.debug("Idtext: %s" % idstext) | |
520 | + req.session['mailarc_mails'] = idstext | |
521 | + req.session['mailarc_category_href'] = self._get_category_href(req,target_category) | |
522 | + | |
523 | + data['messages'] = root_message | |
524 | + data['mls'] = mls; | |
525 | + | |
526 | + add_stylesheet(req, 'mailarchive/css/mailarchive.css') | |
527 | + | |
528 | + return 'mailarchive.html', data, None | |
529 | + | |
530 | + | |
531 | + def _pagelize_list(self,req,results,data): | |
532 | + # get page from req(default page = max_page) | |
533 | + page = int(req.args.get('page', '-1')) | |
534 | + num_item_per_page = int(self.env.config.get('mailarchive', 'items_page','50')) | |
535 | + num_shown_pages = int(self.env.config.get('mailarchive', 'shown_pages','30')) | |
536 | + if page == -1: | |
537 | + results_temp = Paginator(results, 0, num_item_per_page) | |
538 | + page = results_temp.num_pages | |
539 | + | |
540 | + results = Paginator(results, page - 1, num_item_per_page) | |
541 | + | |
542 | + pagedata = [] | |
543 | + data['page_results'] = results | |
544 | + shown_pages = results.get_shown_pages(num_shown_pages) | |
545 | + for shown_page in shown_pages: | |
546 | + page_href = req.href.mailarchive(category=req.args.get('category',None), | |
547 | + page=shown_page, noquickjump=1) | |
548 | + pagedata.append([page_href, None, str(shown_page), | |
549 | + 'page ' + str(shown_page)]) | |
550 | + | |
551 | + fields = ['href', 'class', 'string', 'title'] | |
552 | + results.shown_pages = [dict(zip(fields, p)) for p in pagedata] | |
553 | + | |
554 | + results.current_page = {'href': None, 'class': 'current', | |
555 | + 'string': str(results.page + 1), | |
556 | + 'title':None} | |
557 | + | |
558 | + if results.has_next_page: | |
559 | + next_href = req.href.mailarchive(category=req.args.get('category',None), | |
560 | + page=page + 1) | |
561 | + add_link(req, 'next', next_href, _('Next Page')) | |
562 | + | |
563 | + if results.has_previous_page: | |
564 | + prev_href = req.href.mailarchive(category=req.args.get('category',None), | |
565 | + page=page - 1) | |
566 | + add_link(req, 'prev', prev_href, _('Previous Page')) | |
567 | + | |
568 | + data['page_href'] = req.href.mailarchive(category=req.args.get('category',None)) | |
569 | + return results | |
570 | + | |
571 | + def _serach_parent(self,messageid_to_message,thread_parent): | |
572 | + parents = thread_parent.split(' ') | |
573 | + for parent in parents: | |
574 | + if messageid_to_message.has_key(parent) == True: | |
575 | + self.log.debug('Thread:%s' % parent) | |
576 | + return messageid_to_message[parent] | |
577 | + return messageid_to_message[''] | |
578 | + | |
579 | + | |
580 | + | |
581 | + # IPermissionRequestor method | |
582 | + | |
583 | + def get_permission_actions(self): | |
584 | + return ['MAILARCHIVE_VIEW', | |
585 | + ('MAILARCHIVE_ADMIN', ['MAILARCHIVE_VIEW']), | |
586 | + ] | |
587 | + | |
588 | + # ILegacyAttachmentPolicyDelegate methods | |
589 | + | |
590 | + def check_attachment_permission(self, action, username, resource, perm): | |
591 | + """ Respond to the various actions into the legacy attachment | |
592 | + permissions used by the Attachment module. """ | |
593 | + if resource.parent.realm == 'mailarchive': | |
594 | + if action == 'ATTACHMENT_VIEW': | |
595 | + return 'MAILARCHIVE_VIEW' in perm(resource.parent) | |
596 | + if action in ['ATTACHMENT_CREATE', 'ATTACHMENT_DELETE']: | |
597 | + if 'MAILARCHIVE_ADMIN' in perm(resource.parent): | |
598 | + return True | |
599 | + else: | |
600 | + return True # False | |
601 | + | |
602 | + # IResourceManager methods | |
603 | + | |
604 | + def get_resource_realms(self): | |
605 | + yield 'mailarchive' | |
606 | + | |
607 | + def get_resource_url(self, resource, href, **kwargs): | |
608 | + return href.mailarchive(resource.id) | |
609 | + | |
610 | + def get_resource_description(self, resource, format=None, context=None, | |
611 | + **kwargs): | |
612 | + if context: | |
613 | + return tag.a('mail:'+resource.id, href=context.href.mailarchive(resource.id)) | |
614 | + else: | |
615 | + return 'mail:'+resource.id | |
616 | + | |
617 | + | |
618 | + |
@@ -0,0 +1,72 @@ | ||
1 | +from trac.core import * | |
2 | +from trac import util | |
3 | +from trac.wiki import IWikiSyntaxProvider | |
4 | + | |
5 | + | |
6 | +class WikiSyntaxMail(Component): | |
7 | + implements(IWikiSyntaxProvider) | |
8 | + | |
9 | + # IWikiSyntaxProvider | |
10 | + | |
11 | + def get_link_resolvers(self): | |
12 | + return [('mail', self._format_link)] | |
13 | + | |
14 | + def get_wiki_syntax(self): | |
15 | + yield (r"!?\mail:([0-9]+)", # mail:123 | |
16 | + lambda x, y, z: self._format_link(x, 'mail', y[5:], y)) | |
17 | + | |
18 | + def _format_link(self, formatter, ns, target, label): | |
19 | + cursor = formatter.db.cursor() | |
20 | + cursor.execute("SELECT subject,id FROM mailarc WHERE id = %s" , (target,)) | |
21 | + row = cursor.fetchone() | |
22 | + if row: | |
23 | + subject = util.escape(util.shorten_line(row[0])) | |
24 | + return '<a href="%s" title="%s">%s</a>' \ | |
25 | + % (formatter.href.mailarchive(row[1]), subject, label) | |
26 | + else: | |
27 | + return label | |
28 | + | |
29 | +class WikiSyntaxMl(Component): | |
30 | + implements(IWikiSyntaxProvider) | |
31 | + | |
32 | + # IWikiSyntaxProvider | |
33 | + | |
34 | + def get_link_resolvers(self): | |
35 | + return [('ml', self._format_link)] | |
36 | + | |
37 | + def get_wiki_syntax(self): | |
38 | + yield (r"!?\[(.+?)[ :]([0-9]+)\]", # [xxx 123] or [aaa:123] | |
39 | + lambda x, y, z: self._format_link(x, 'ml', y[1:1], y)) | |
40 | + | |
41 | + def _format_link(self, formatter, ns, target, label): | |
42 | + cursor = formatter.db.cursor() | |
43 | + cursor.execute("SELECT subject,id FROM mailarc WHERE subject like '%s%%'" % label) | |
44 | + row = cursor.fetchone() | |
45 | + if row: | |
46 | + subject = util.escape(util.shorten_line(row[0])) | |
47 | + return '<a href="%s" title="%s">%s</a>' \ | |
48 | + % (formatter.href.mailarchive(row[1]), subject, label) | |
49 | + else: | |
50 | + return label | |
51 | + | |
52 | +class WikiSyntaxMessageId(Component): | |
53 | + implements(IWikiSyntaxProvider) | |
54 | + | |
55 | + def get_link_resolvers(self): | |
56 | + return [('messageid', self._format_link)] | |
57 | + | |
58 | + def get_wiki_syntax(self): | |
59 | + yield (r"!?Message-ID:<(.+?)>", # Message-ID:<aaa> | |
60 | + lambda x, y, z: self._format_link(x, 'messageid' ,y[12:-1],y)) | |
61 | + | |
62 | + def _format_link(self, formatter, ns, target, label): | |
63 | + cursor = formatter.db.cursor() | |
64 | + cursor.execute("SELECT subject,id FROM mailarc WHERE messageid = %s" , (target,)) | |
65 | + row = cursor.fetchone() | |
66 | + if row: | |
67 | + subject = util.escape(util.shorten_line(row[0])) | |
68 | + return '<a href="%s" title="%s">%s</a>' \ | |
69 | + % (formatter.href.mailarchive(row[1]), subject, label) | |
70 | + else: | |
71 | + return label | |
72 | + |
@@ -0,0 +1,43 @@ | ||
1 | +# -*- coding: utf-8 -*- | |
2 | +# MailArchive plugin | |
3 | + | |
4 | +from datetime import datetime,timedelta | |
5 | +import uuid | |
6 | + | |
7 | + | |
8 | +now = datetime.now() | |
9 | +today = datetime.today() | |
10 | + | |
11 | +_mail_num = 1000 | |
12 | +i = 0 | |
13 | + | |
14 | +mail_address="testmail@example.com" | |
15 | +dt = datetime.now() | |
16 | +message_id = '' | |
17 | + | |
18 | +for i in range(0,_mail_num): | |
19 | + last_message_id = message_id | |
20 | + if i % 5 == 0: | |
21 | + last_message_id = '' | |
22 | + message_id = '%s%s'%(uuid.uuid4() , mail_address) | |
23 | + dt = dt + timedelta(0,60) | |
24 | + print "From - %s"%(dt.strftime('%a %b %d %H:%M:%S %Y')) #Mon Jun 30 14:29:49 2008 | |
25 | + print "Received: by 192.168.0.1 with HTTP; Sun, 29 Jun 2008 22:26:59 -0700 (PDT)" | |
26 | + print "Message-ID: <%s>" %( message_id ) | |
27 | + print "Date: %s +0900"%(dt.strftime('%a, %d %b %Y %H:%M:%S')) #Mon, 30 Jun 2008 14:26:59 +0900 | |
28 | + print "From: %s"%(mail_address) | |
29 | + print "To: %s"%(mail_address) | |
30 | + print "Subject: TestII%s "%(str(i)) | |
31 | + if last_message_id != '': | |
32 | + print "In-Reply-To: <%s>" %( last_message_id ) | |
33 | + print "MIME-Version: 1.0" | |
34 | + print "Content-Type: text/plain; charset=ISO-8859-1" | |
35 | + print "Content-Transfer-Encoding: 7bit" | |
36 | + print "Content-Disposition: inline" | |
37 | + print "Delivered-To: mailarchivetest@example.com" | |
38 | + print "" | |
39 | + print "This is Test Mail ." | |
40 | + print "" | |
41 | + print "" | |
42 | + | |
43 | + |
@@ -0,0 +1,24 @@ | ||
1 | +.thread_ul { padding-left: 20px; margin-left:0px;} | |
2 | +.thread_li { padding-left: 0px; margin-left:0px;} | |
3 | +.thread_subject_clip { | |
4 | + padding-right:20px; | |
5 | + background-image: url(../png/clip.png); | |
6 | + background-repeat: no-repeat; | |
7 | + background-position: right top; | |
8 | +} | |
9 | +.thread_from { font-size: 80%; color: #666} | |
10 | +.thread_senddate { font-size: 80%; color: #666} | |
11 | +#prefs ul {padding-left: 10px; margin:0px 3px; } | |
12 | +#prefs li {padding-left: 0px; margin:0px 3px; } | |
13 | +#prefs .prefs_from { color: #666} | |
14 | +#prefs {width:120px;} | |
15 | +* html #prefs { width: 14em } /* Set width only for IE */ | |
16 | + | |
17 | + | |
18 | +/* | |
19 | +** Style used for displaying Mail icon in Timeline | |
20 | +*/ | |
21 | + | |
22 | +.timeline dt.mailarchive a { | |
23 | + background-image: url(../png/mail.png); | |
24 | +} |
@@ -0,0 +1,55 @@ | ||
1 | +<!DOCTYPE html | |
2 | + PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" | |
3 | + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> | |
4 | +<html xmlns="http://www.w3.org/1999/xhtml" | |
5 | + xmlns:py="http://genshi.edgewall.org/" | |
6 | + xmlns:xi="http://www.w3.org/2001/XInclude"> | |
7 | + <xi:include href="layout.html" /> | |
8 | + <xi:include href="macros.html" /> | |
9 | + <head> | |
10 | + <title>MailArchive - $message.subject</title> | |
11 | + <script type="text/javascript"> | |
12 | + jQuery(document).ready(function($) { | |
13 | + $("#content").find("h1,h2,h3,h4,h5,h6").addAnchor("${_('Link to this section')}"); | |
14 | + }); | |
15 | + </script> | |
16 | + </head> | |
17 | + | |
18 | + <body> | |
19 | + <div id="content" class="mailarchive"> | |
20 | + | |
21 | + | |
22 | + <form py:if="len(reflist) >=2" id="prefs" method="get" action=""> | |
23 | + <label >関連するメール:</label> | |
24 | + <ul> | |
25 | + <py:for each="refmail in reflist"> | |
26 | + <li> | |
27 | + <py:choose test="refmail.href!=''"> | |
28 | + <py:when test="True"> | |
29 | + <a class="subject" href="$refmail.href" alt="$refmail.subject_alt">$refmail.subject</a> | |
30 | + </py:when> | |
31 | + <py:otherwise> | |
32 | + <strong alt="$refmail.subject_alt">$refmail.subject</strong> | |
33 | + </py:otherwise> | |
34 | + </py:choose> | |
35 | + <span class="prefs_from" >$refmail.fromname</span></li> | |
36 | + </py:for> | |
37 | + </ul> | |
38 | + </form> | |
39 | + | |
40 | + <h2>$message.subject</h2> | |
41 | + <ul> | |
42 | + <li>From:<a class="mail-link" href="mailto:$message.fromaddr"><span class="icon">$message.fromname</span></a></li> | |
43 | + <li>Date:$message.senddate</li> | |
44 | + </ul> | |
45 | + | |
46 | + <div class="mailarcpage searchable">$message.page_html</div> | |
47 | + | |
48 | + ${list_of_attachments(attachments,True)} | |
49 | + | |
50 | +</div> | |
51 | + | |
52 | + </body> | |
53 | +</html> | |
54 | + | |
55 | + |
@@ -0,0 +1,83 @@ | ||
1 | +<!DOCTYPE html | |
2 | + PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" | |
3 | + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> | |
4 | +<html xmlns="http://www.w3.org/1999/xhtml" | |
5 | + xmlns:py="http://genshi.edgewall.org/" | |
6 | + xmlns:xi="http://www.w3.org/2001/XInclude"> | |
7 | + <xi:include href="layout.html" /> | |
8 | + <xi:include href="macros.html" /> | |
9 | + <head> | |
10 | + <title>${_('MailArchive')}</title> | |
11 | + </head> | |
12 | + | |
13 | + <body> | |
14 | + | |
15 | + <div id="content" class="mailarc"> | |
16 | + <form id="prefs" method="get" action=""> | |
17 | + <py:for each="ml in mls"> | |
18 | + <div class="category_name">$ml.name</div> | |
19 | + <ul> | |
20 | + <py:for each="subitem in ml.yearmonths"> | |
21 | + <li class="category_li"> | |
22 | + <py:choose test="subitem.href"> | |
23 | + <py:when test=""> | |
24 | + <a href="$subitem.href">$subitem.year/$subitem.month ($subitem.count)</a> | |
25 | + </py:when> | |
26 | + <py:otherwise> | |
27 | + $subitem.year/$subitem.month ($subitem.count) | |
28 | + </py:otherwise> | |
29 | + </py:choose> | |
30 | + </li> | |
31 | + </py:for> | |
32 | + </ul> | |
33 | + </py:for> | |
34 | + </form> | |
35 | + | |
36 | + <h2> | |
37 | + $nameの$year年$month月のメール <span class="numresults" py:if="page_results">(${page_results.displayed_items()})</span> | |
38 | + </h2> | |
39 | + | |
40 | + | |
41 | +<py:def function="mailarc_row(rows,depth)"> | |
42 | + <py:for each="message in rows"> | |
43 | + <li class="thread_li"> | |
44 | + <py:choose> | |
45 | + <a py:when="message.attachment" class="thread_subject_clip" | |
46 | + href="$message.mail_href">$message.subject</a> | |
47 | + <a py:otherwise="" | |
48 | + href="$message.mail_href">$message.subject</a> | |
49 | + </py:choose> | |
50 | + <br /> | |
51 | + | |
52 | + <span class="thread_from">$message.fromname</span> | |
53 | + <span class="thread_senddate">$message.senddate</span> | |
54 | + | |
55 | + <ul py:if="len(message.children)>0" class="thread_ul"> | |
56 | + ${mailarc_row(message.children,depth+1)} | |
57 | + </ul> | |
58 | + </li> | |
59 | + </py:for> | |
60 | +</py:def> | |
61 | + | |
62 | +<xi:include py:with="paginator = page_results" href="page_index.html" /> | |
63 | +<div id="mail_thread"> | |
64 | +<ul py:if="len(messages.children)>0" class="thread_ul"> | |
65 | +${mailarc_row(messages.children,0)} | |
66 | +</ul> | |
67 | +</div> | |
68 | +<xi:include py:with="paginator = page_results" href="page_index.html" /> | |
69 | + | |
70 | + | |
71 | + | |
72 | + | |
73 | + | |
74 | + | |
75 | + | |
76 | + <div id="help"> | |
77 | + </div> | |
78 | + | |
79 | + </div> | |
80 | + </body> | |
81 | +</html> | |
82 | + | |
83 | + |
@@ -0,0 +1,4 @@ | ||
1 | +# MailArchive module | |
2 | +from mailarchive import * | |
3 | +from wikisyntax import * | |
4 | +from mailarchiveadmin import * | |
\ No newline at end of file |
@@ -0,0 +1,16 @@ | ||
1 | +from setuptools import find_packages,setup | |
2 | + | |
3 | +setup( | |
4 | + name='TracMailArchive', version='0.12.0.1', | |
5 | + packages=find_packages(exclude=['*.tests*']), | |
6 | + entry_points = """ | |
7 | + [trac.plugins] | |
8 | + TracMailArchive = mailarchive | |
9 | + """, | |
10 | + package_data={'mailarchive': ['templates/*.html', | |
11 | + 'htdocs/css/*.css', | |
12 | + 'htdocs/png/*']}, | |
13 | + install_requires = [ | |
14 | + 'Trac>=0.12', | |
15 | + ], | |
16 | +) |
@@ -0,0 +1,28 @@ | ||
1 | +Copyright (c) 2007, Kazuya Hirobe. | |
2 | +All rights reserved. | |
3 | + | |
4 | +Redistribution and use in source and binary forms, with or without | |
5 | +modification, are permitted provided that the following conditions | |
6 | +are met: | |
7 | + | |
8 | + 1. Redistributions of source code must retain the above copyright | |
9 | + notice, this list of conditions and the following disclaimer. | |
10 | + 2. Redistributions in binary form must reproduce the above copyright | |
11 | + notice, this list of conditions and the following disclaimer in | |
12 | + the documentation and/or other materials provided with the | |
13 | + distribution. | |
14 | + 3. The name of the author may not be used to endorse or promote | |
15 | + products derived from this software without specific prior | |
16 | + written permission. | |
17 | + | |
18 | +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS | |
19 | +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | |
20 | +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
21 | +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY | |
22 | +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
23 | +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE | |
24 | +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
25 | +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER | |
26 | +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR | |
27 | +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN | |
28 | +IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |