| 1 |
|
|---|
| 2 |
|
|---|
| 3 |
|
|---|
| 4 |
|
|---|
| 5 |
|
|---|
| 6 |
|
|---|
| 7 |
|
|---|
| 8 |
|
|---|
| 9 |
|
|---|
| 10 |
|
|---|
| 11 |
|
|---|
| 12 |
|
|---|
| 13 |
|
|---|
| 14 |
|
|---|
| 15 |
|
|---|
| 16 |
|
|---|
| 17 |
|
|---|
| 18 |
|
|---|
| 19 |
|
|---|
| 20 |
|
|---|
| 21 |
|
|---|
| 22 |
""" |
|---|
| 23 |
email2trac.py -- Email tickets to Trac. |
|---|
| 24 |
|
|---|
| 25 |
A simple MTA filter to create Trac tickets from inbound emails. |
|---|
| 26 |
|
|---|
| 27 |
Copyright 2005, Daniel Lundin <daniel@edgewall.com> |
|---|
| 28 |
Copyright 2005, Edgewall Software |
|---|
| 29 |
|
|---|
| 30 |
Changed By: Bas van der Vlies <basv@sara.nl> |
|---|
| 31 |
Date : 13 September 2005 |
|---|
| 32 |
Descr. : Added config file and command line options, spam level |
|---|
| 33 |
detection, reply address and mailto option. Unicode support |
|---|
| 34 |
|
|---|
| 35 |
Changed By: Walter de Jong <walter@sara.nl> |
|---|
| 36 |
Descr. : multipart-message code and trac attachments |
|---|
| 37 |
|
|---|
| 38 |
Changed By: Franco (nextime) Lanza <nextime@nexlab.it> |
|---|
| 39 |
Date: 18/01/2007 |
|---|
| 40 |
Descr. : Some semplifications on the script. |
|---|
| 41 |
|
|---|
| 42 |
|
|---|
| 43 |
The scripts reads emails from stdin and inserts directly into a Trac database. |
|---|
| 44 |
MIME headers are mapped as follows: |
|---|
| 45 |
|
|---|
| 46 |
* From: => Reporter |
|---|
| 47 |
=> CC (Optional via reply_address option) |
|---|
| 48 |
* Subject: => Summary |
|---|
| 49 |
* Body => Description |
|---|
| 50 |
* Component => Can be set to SPAM via spam_level option |
|---|
| 51 |
|
|---|
| 52 |
How to use |
|---|
| 53 |
---------- |
|---|
| 54 |
* Create an config file: |
|---|
| 55 |
[DEFAULT] # REQUIRED |
|---|
| 56 |
project : /data/trac/test # REQUIRED |
|---|
| 57 |
debug : 1 # OPTIONAL, if set print some DEBUG info |
|---|
| 58 |
reply_address: 1 # OPTIONAL, if set then fill in ticket CC field |
|---|
| 59 |
umask : 022 # OPTIONAL, if set then use this umask for creation of the attachments |
|---|
| 60 |
mailto_link : 1 # OPTIONAL, if set then [mailto:<>] in description |
|---|
| 61 |
myaddress : address@trac.tld |
|---|
| 62 |
mailto_cc : basv@sara.nl # OPTIONAL, use this address as CC in mailto line |
|---|
| 63 |
ticket_update: 1 # OPTIONAL, if set then check if this is an update for a ticket |
|---|
| 64 |
trac_version : 0.9 # OPTIONAL, default is 0.10 |
|---|
| 65 |
|
|---|
| 66 |
[jouvin] # OPTIONAL project declaration, if set both fields necessary |
|---|
| 67 |
project : /data/trac/jouvin # use -p|--project jouvin. |
|---|
| 68 |
myaddress : ticket@jouin.tld |
|---|
| 69 |
|
|---|
| 70 |
* default config file is : /etc/email2trac.conf |
|---|
| 71 |
|
|---|
| 72 |
* Commandline opions: |
|---|
| 73 |
-h | --help |
|---|
| 74 |
-c <value> | --component=<value> |
|---|
| 75 |
-f <config file> | --file=<config file> |
|---|
| 76 |
-p <project name> | --project=<project name> |
|---|
| 77 |
|
|---|
| 78 |
""" |
|---|
| 79 |
import os |
|---|
| 80 |
import sys |
|---|
| 81 |
import string |
|---|
| 82 |
import getopt |
|---|
| 83 |
import stat |
|---|
| 84 |
import time |
|---|
| 85 |
import email |
|---|
| 86 |
import email.Iterators |
|---|
| 87 |
import email.Header |
|---|
| 88 |
import re |
|---|
| 89 |
import urllib |
|---|
| 90 |
import unicodedata |
|---|
| 91 |
import ConfigParser |
|---|
| 92 |
from stat import * |
|---|
| 93 |
import mimetypes |
|---|
| 94 |
import syslog |
|---|
| 95 |
import traceback |
|---|
| 96 |
|
|---|
| 97 |
|
|---|
| 98 |
|
|---|
| 99 |
|
|---|
| 100 |
trac_default_version = 0.10 |
|---|
| 101 |
m = None |
|---|
| 102 |
|
|---|
| 103 |
|
|---|
| 104 |
class TicketEmailParser(object): |
|---|
| 105 |
env = None |
|---|
| 106 |
comment = '> ' |
|---|
| 107 |
|
|---|
| 108 |
def __init__(self, env, parameters, version): |
|---|
| 109 |
self.env = env |
|---|
| 110 |
|
|---|
| 111 |
|
|---|
| 112 |
|
|---|
| 113 |
self.db = None |
|---|
| 114 |
|
|---|
| 115 |
|
|---|
| 116 |
|
|---|
| 117 |
self.author = None |
|---|
| 118 |
self.email_addr = None |
|---|
| 119 |
self.email_field = None |
|---|
| 120 |
|
|---|
| 121 |
self.VERSION = version |
|---|
| 122 |
if self.VERSION == 0.8: |
|---|
| 123 |
self.get_config = self.env.get_config |
|---|
| 124 |
else: |
|---|
| 125 |
self.get_config = self.env.config.get |
|---|
| 126 |
|
|---|
| 127 |
if parameters.has_key('umask'): |
|---|
| 128 |
os.umask(int(parameters['umask'], 8)) |
|---|
| 129 |
|
|---|
| 130 |
if parameters.has_key('myaddress'): |
|---|
| 131 |
self.myaddress = parameters['myaddress'] |
|---|
| 132 |
else: |
|---|
| 133 |
self.myaddress = None |
|---|
| 134 |
|
|---|
| 135 |
if parameters.has_key('debug'): |
|---|
| 136 |
self.DEBUG = int(parameters['debug']) |
|---|
| 137 |
else: |
|---|
| 138 |
self.DEBUG = 0 |
|---|
| 139 |
|
|---|
| 140 |
if parameters.has_key('mailto_link'): |
|---|
| 141 |
self.MAILTO = int(parameters['mailto_link']) |
|---|
| 142 |
if parameters.has_key('mailto_cc'): |
|---|
| 143 |
self.MAILTO_CC = parameters['mailto_cc'] |
|---|
| 144 |
else: |
|---|
| 145 |
self.MAILTO_CC = '' |
|---|
| 146 |
else: |
|---|
| 147 |
self.MAILTO = 0 |
|---|
| 148 |
|
|---|
| 149 |
if parameters.has_key('email_comment'): |
|---|
| 150 |
self.comment = str(parameters['email_comment']) |
|---|
| 151 |
|
|---|
| 152 |
if parameters.has_key('email_header'): |
|---|
| 153 |
self.EMAIL_HEADER = int(parameters['email_header']) |
|---|
| 154 |
else: |
|---|
| 155 |
self.EMAIL_HEADER = 0 |
|---|
| 156 |
|
|---|
| 157 |
if parameters.has_key('alternate_notify_template'): |
|---|
| 158 |
self.notify_template = str(parameters['alternate_notify_template']) |
|---|
| 159 |
else: |
|---|
| 160 |
self.notify_template = None |
|---|
| 161 |
|
|---|
| 162 |
if parameters.has_key('reply_all'): |
|---|
| 163 |
self.REPLY_ALL = int(parameters['reply_all']) |
|---|
| 164 |
else: |
|---|
| 165 |
self.REPLY_ALL = 0 |
|---|
| 166 |
|
|---|
| 167 |
if parameters.has_key('ticket_update'): |
|---|
| 168 |
self.TICKET_UPDATE = int(parameters['ticket_update']) |
|---|
| 169 |
else: |
|---|
| 170 |
self.TICKET_UPDATE = 0 |
|---|
| 171 |
|
|---|
| 172 |
if parameters.has_key('drop_spam'): |
|---|
| 173 |
self.DROP_SPAM = int(parameters['drop_spam']) |
|---|
| 174 |
else: |
|---|
| 175 |
self.DROP_SPAM = 0 |
|---|
| 176 |
|
|---|
| 177 |
if parameters.has_key('verbatim_format'): |
|---|
| 178 |
self.VERBATIM_FORMAT = int(parameters['verbatim_format']) |
|---|
| 179 |
else: |
|---|
| 180 |
self.VERBATIM_FORMAT = 1 |
|---|
| 181 |
|
|---|
| 182 |
if parameters.has_key('strip_signature'): |
|---|
| 183 |
self.STRIP_SIGNATURE = int(parameters['strip_signature']) |
|---|
| 184 |
else: |
|---|
| 185 |
self.STRIP_SIGNATURE = 0 |
|---|
| 186 |
|
|---|
| 187 |
def spam(self, message): |
|---|
| 188 |
if message.has_key('X-Spam-Status'): |
|---|
| 189 |
spamvalue = message['X-Spam-Status'].split(",")[0] |
|---|
| 190 |
if spamvalue == "Yes": |
|---|
| 191 |
return 'Spam' |
|---|
| 192 |
|
|---|
| 193 |
elif message.has_key('X-Virus-found'): |
|---|
| 194 |
return 'Spam' |
|---|
| 195 |
|
|---|
| 196 |
return self.get_config('ticket', 'default_component') |
|---|
| 197 |
|
|---|
| 198 |
def email_to_unicode(self, message_str): |
|---|
| 199 |
""" |
|---|
| 200 |
Email has 7 bit ASCII code, convert it to unicode with the charset |
|---|
| 201 |
that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac |
|---|
| 202 |
understands it. |
|---|
| 203 |
""" |
|---|
| 204 |
results = email.Header.decode_header(message_str) |
|---|
| 205 |
str = None |
|---|
| 206 |
for text,format in results: |
|---|
| 207 |
if format: |
|---|
| 208 |
try: |
|---|
| 209 |
temp = unicode(text, format) |
|---|
| 210 |
except UnicodeError, detail: |
|---|
| 211 |
|
|---|
| 212 |
|
|---|
| 213 |
temp = unicode(text, 'iso-8859-15') |
|---|
| 214 |
except LookupError, detail: |
|---|
| 215 |
|
|---|
| 216 |
|
|---|
| 217 |
temp = message_str |
|---|
| 218 |
|
|---|
| 219 |
else: |
|---|
| 220 |
temp = string.strip(text) |
|---|
| 221 |
temp = unicode(text, 'iso-8859-15') |
|---|
| 222 |
|
|---|
| 223 |
if str: |
|---|
| 224 |
str = '%s %s' %(str, temp) |
|---|
| 225 |
else: |
|---|
| 226 |
str = '%s' %temp |
|---|
| 227 |
|
|---|
| 228 |
|
|---|
| 229 |
return str |
|---|
| 230 |
|
|---|
| 231 |
def debug_attachments(self, message): |
|---|
| 232 |
n = 0 |
|---|
| 233 |
for part in message.walk(): |
|---|
| 234 |
if part.get_content_maintype() == 'multipart': |
|---|
| 235 |
print 'TD: multipart container' |
|---|
| 236 |
continue |
|---|
| 237 |
|
|---|
| 238 |
n = n + 1 |
|---|
| 239 |
print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type()) |
|---|
| 240 |
print 'TD: part%d: filename: %s' % (n, part.get_filename()) |
|---|
| 241 |
|
|---|
| 242 |
if part.is_multipart(): |
|---|
| 243 |
print 'TD: this part is multipart' |
|---|
| 244 |
payload = part.get_payload(decode=1) |
|---|
| 245 |
print 'TD: payload:', payload |
|---|
| 246 |
else: |
|---|
| 247 |
print 'TD: this part is not multipart' |
|---|
| 248 |
|
|---|
| 249 |
part_file = '/tmp/part%d' % n |
|---|
| 250 |
print 'TD: writing part%d (%s)' % (n,part_file) |
|---|
| 251 |
fx = open(part_file, 'wb') |
|---|
| 252 |
text = part.get_payload(decode=1) |
|---|
| 253 |
if not text: |
|---|
| 254 |
text = '(None)' |
|---|
| 255 |
fx.write(text) |
|---|
| 256 |
fx.close() |
|---|
| 257 |
try: |
|---|
| 258 |
os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO) |
|---|
| 259 |
except OSError: |
|---|
| 260 |
pass |
|---|
| 261 |
|
|---|
| 262 |
def email_header_txt(self, m): |
|---|
| 263 |
""" |
|---|
| 264 |
Display To and CC addresses in description field |
|---|
| 265 |
""" |
|---|
| 266 |
str = '' |
|---|
| 267 |
if m['To'] and len(m['To']) > 0 and m['To'] != 'hic@sara.nl': |
|---|
| 268 |
str = "'''To:''' %s [[BR]]" %(m['To']) |
|---|
| 269 |
if m['Cc'] and len(m['Cc']) > 0: |
|---|
| 270 |
str = "%s'''Cc:''' %s [[BR]]" % (str, m['Cc']) |
|---|
| 271 |
|
|---|
| 272 |
return self.email_to_unicode(str) |
|---|
| 273 |
|
|---|
| 274 |
|
|---|
| 275 |
def set_owner(self, ticket): |
|---|
| 276 |
""" |
|---|
| 277 |
Select default owner for ticket component |
|---|
| 278 |
""" |
|---|
| 279 |
cursor = self.db.cursor() |
|---|
| 280 |
sql = "SELECT owner FROM component WHERE name='%s'" % ticket['component'] |
|---|
| 281 |
cursor.execute(sql) |
|---|
| 282 |
try: |
|---|
| 283 |
ticket['owner'] = cursor.fetchone()[0] |
|---|
| 284 |
except TypeError, detail: |
|---|
| 285 |
ticket['owner'] = "UNKNOWN" |
|---|
| 286 |
|
|---|
| 287 |
def get_author_emailaddrs(self, message): |
|---|
| 288 |
""" |
|---|
| 289 |
Get the default author name and email address from the message |
|---|
| 290 |
""" |
|---|
| 291 |
temp = self.email_to_unicode(message['from']) |
|---|
| 292 |
|
|---|
| 293 |
|
|---|
| 294 |
self.author, self.email_addr = email.Utils.parseaddr(temp) |
|---|
| 295 |
|
|---|
| 296 |
|
|---|
| 297 |
if self.email_addr == self.myaddress: |
|---|
| 298 |
sys.exit(0) |
|---|
| 299 |
|
|---|
| 300 |
|
|---|
| 301 |
|
|---|
| 302 |
|
|---|
| 303 |
if self.VERSION == 0.8: |
|---|
| 304 |
users = [] |
|---|
| 305 |
else: |
|---|
| 306 |
users = [ u for (u, n, e) in self.env.get_known_users(self.db) |
|---|
| 307 |
if e == self.email_addr ] |
|---|
| 308 |
|
|---|
| 309 |
if len(users) == 1: |
|---|
| 310 |
self.email_field = users[0] |
|---|
| 311 |
else: |
|---|
| 312 |
self.email_field = self.email_to_unicode(message['from']) |
|---|
| 313 |
|
|---|
| 314 |
def set_reply_fields(self, ticket, message): |
|---|
| 315 |
""" |
|---|
| 316 |
Set all the right fields for a new ticket |
|---|
| 317 |
""" |
|---|
| 318 |
ticket['reporter'] = self.email_field |
|---|
| 319 |
|
|---|
| 320 |
|
|---|
| 321 |
|
|---|
| 322 |
if self.REPLY_ALL: |
|---|
| 323 |
|
|---|
| 324 |
ccs = message.get_all('cc', []) |
|---|
| 325 |
|
|---|
| 326 |
addrs = email.Utils.getaddresses(ccs) |
|---|
| 327 |
if not addrs: |
|---|
| 328 |
return |
|---|
| 329 |
|
|---|
| 330 |
|
|---|
| 331 |
|
|---|
| 332 |
|
|---|
| 333 |
if self.notification: |
|---|
| 334 |
try: |
|---|
| 335 |
addrs.remove((self.author, self.email_addr)) |
|---|
| 336 |
except ValueError, detail: |
|---|
| 337 |
pass |
|---|
| 338 |
|
|---|
| 339 |
for name,mail in addrs: |
|---|
| 340 |
|
|---|
| 341 |
|
|---|
| 342 |
if mail != self.myaddress: |
|---|
| 343 |
try: |
|---|
| 344 |
mail_list = '%s, %s' %(mail_list, mail) |
|---|
| 345 |
except: |
|---|
| 346 |
mail_list = mail |
|---|
| 347 |
|
|---|
| 348 |
if mail_list: |
|---|
| 349 |
ticket['cc'] = self.email_to_unicode(mail_list) |
|---|
| 350 |
|
|---|
| 351 |
def save_email_for_debug(self, message, tempfile=False): |
|---|
| 352 |
if tempfile: |
|---|
| 353 |
import tempfile |
|---|
| 354 |
msg_file = tempfile.mktemp('.email2trac') |
|---|
| 355 |
else: |
|---|
| 356 |
msg_file = '/tmp/msg.txt' |
|---|
| 357 |
print 'TD: saving email to %s' % msg_file |
|---|
| 358 |
fx = open(msg_file, 'wb') |
|---|
| 359 |
fx.write('%s' % message) |
|---|
| 360 |
fx.close() |
|---|
| 361 |
try: |
|---|
| 362 |
os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO) |
|---|
| 363 |
except OSError: |
|---|
| 364 |
pass |
|---|
| 365 |
|
|---|
| 366 |
def ticket_update(self, m): |
|---|
| 367 |
""" |
|---|
| 368 |
If the current email is a reply to an existing ticket, this function |
|---|
| 369 |
will append the contents of this email to that ticket, instead of |
|---|
| 370 |
creating a new one. |
|---|
| 371 |
""" |
|---|
| 372 |
if not m['Subject']: |
|---|
| 373 |
return False |
|---|
| 374 |
else: |
|---|
| 375 |
subject = self.email_to_unicode(m['Subject']) |
|---|
| 376 |
|
|---|
| 377 |
TICKET_RE = re.compile(r""" |
|---|
| 378 |
(?P<ticketnr>[#][0-9]+:) |
|---|
| 379 |
""", re.VERBOSE) |
|---|
| 380 |
|
|---|
| 381 |
result = TICKET_RE.search(subject) |
|---|
| 382 |
if not result: |
|---|
| 383 |
return False |
|---|
| 384 |
|
|---|
| 385 |
body_text = self.get_body_text(m) |
|---|
| 386 |
|
|---|
| 387 |
|
|---|
| 388 |
|
|---|
| 389 |
ticket_id = result.group('ticketnr') |
|---|
| 390 |
ticket_id = int(ticket_id[1:-1]) |
|---|
| 391 |
|
|---|
| 392 |
|
|---|
| 393 |
|
|---|
| 394 |
when = int(time.time()) |
|---|
| 395 |
|
|---|
| 396 |
if self.VERSION == 0.8: |
|---|
| 397 |
tkt = Ticket(self.db, ticket_id) |
|---|
| 398 |
tkt.save_changes(self.db, self.author, body_text, when) |
|---|
| 399 |
else: |
|---|
| 400 |
try: |
|---|
| 401 |
tkt = Ticket(self.env, ticket_id, self.db) |
|---|
| 402 |
except util.TracError, detail: |
|---|
| 403 |
return False |
|---|
| 404 |
|
|---|
| 405 |
tkt.save_changes(self.author, body_text, when) |
|---|
| 406 |
tkt['id'] = ticket_id |
|---|
| 407 |
|
|---|
| 408 |
if (self.VERSION == 0.9) or (self.VERSION == 0.10): |
|---|
| 409 |
self.attachments(m, tkt, True) |
|---|
| 410 |
else: |
|---|
| 411 |
self.attachments(m, tkt) |
|---|
| 412 |
|
|---|
| 413 |
if self.notification: |
|---|
| 414 |
self.notify(tkt, False, when) |
|---|
| 415 |
|
|---|
| 416 |
return True |
|---|
| 417 |
|
|---|
| 418 |
def new_ticket(self, msg): |
|---|
| 419 |
""" |
|---|
| 420 |
Create a new ticket |
|---|
| 421 |
""" |
|---|
| 422 |
tkt = Ticket(self.env) |
|---|
| 423 |
tkt['status'] = 'new' |
|---|
| 424 |
|
|---|
| 425 |
|
|---|
| 426 |
|
|---|
| 427 |
tkt['milestone'] = self.get_config('ticket', 'default_milestone') |
|---|
| 428 |
tkt['priority'] = self.get_config('ticket', 'default_priority') |
|---|
| 429 |
tkt['severity'] = self.get_config('ticket', 'default_severity') |
|---|
| 430 |
tkt['version'] = self.get_config('ticket', 'default_version') |
|---|
| 431 |
|
|---|
| 432 |
if not msg['Subject']: |
|---|
| 433 |
tkt['summary'] = u'(geen subject)' |
|---|
| 434 |
else: |
|---|
| 435 |
tkt['summary'] = self.email_to_unicode(msg['Subject']) |
|---|
| 436 |
|
|---|
| 437 |
|
|---|
| 438 |
if settings.has_key('component'): |
|---|
| 439 |
tkt['component'] = settings['component'] |
|---|
| 440 |
else: |
|---|
| 441 |
tkt['component'] = self.spam(msg) |
|---|
| 442 |
|
|---|
| 443 |
|
|---|
| 444 |
|
|---|
| 445 |
if self.DROP_SPAM and (tkt['component'] == 'Spam'): |
|---|
| 446 |
|
|---|
| 447 |
return False |
|---|
| 448 |
|
|---|
| 449 |
|
|---|
| 450 |
|
|---|
| 451 |
self.set_owner(tkt) |
|---|
| 452 |
self.set_reply_fields(tkt, msg) |
|---|
| 453 |
|
|---|
| 454 |
|
|---|
| 455 |
|
|---|
| 456 |
head = '' |
|---|
| 457 |
if self.EMAIL_HEADER > 0: |
|---|
| 458 |
head = self.email_header_txt(msg) |
|---|
| 459 |
|
|---|
| 460 |
body_text = self.get_body_text(msg) |
|---|
| 461 |
|
|---|
| 462 |
tkt['description'] = '\r\n%s\r\n%s' \ |
|---|
| 463 |
%(head, body_text) |
|---|
| 464 |
|
|---|
| 465 |
when = int(time.time()) |
|---|
| 466 |
if self.VERSION == 0.8: |
|---|
| 467 |
ticket_id = tkt.insert(self.db) |
|---|
| 468 |
else: |
|---|
| 469 |
ticket_id = tkt.insert() |
|---|
| 470 |
tkt['id'] = ticket_id |
|---|
| 471 |
|
|---|
| 472 |
changed = False |
|---|
| 473 |
comment = '' |
|---|
| 474 |
|
|---|
| 475 |
|
|---|
| 476 |
|
|---|
| 477 |
if self.MAILTO: |
|---|
| 478 |
changed = True |
|---|
| 479 |
comment = u'\nadded mailto line\n' |
|---|
| 480 |
mailto = self.html_mailto_link(tkt['summary'], ticket_id, body_text) |
|---|
| 481 |
tkt['description'] = u'\r\n%s\r\n%s%s\r\n' \ |
|---|
| 482 |
%(head, mailto, body_text) |
|---|
| 483 |
|
|---|
| 484 |
n = self.attachments(msg, tkt) |
|---|
| 485 |
if n: |
|---|
| 486 |
changed = True |
|---|
| 487 |
comment = '%s\nThis message has %d attachment(s)\n' %(comment, n) |
|---|
| 488 |
|
|---|
| 489 |
if changed: |
|---|
| 490 |
if self.VERSION == 0.8: |
|---|
| 491 |
tkt.save_changes(self.db, self.author, comment) |
|---|
| 492 |
else: |
|---|
| 493 |
tkt.save_changes(self.author, comment) |
|---|
| 494 |
|
|---|
| 495 |
|
|---|
| 496 |
|
|---|
| 497 |
if self.notification: |
|---|
| 498 |
self.notify(tkt, True) |
|---|
| 499 |
|
|---|
| 500 |
|
|---|
| 501 |
def parse(self, fp): |
|---|
| 502 |
global m |
|---|
| 503 |
|
|---|
| 504 |
m = email.message_from_file(fp) |
|---|
| 505 |
if not m: |
|---|
| 506 |
return |
|---|
| 507 |
|
|---|
| 508 |
if self.DEBUG > 1: |
|---|
| 509 |
self.save_email_for_debug(m) |
|---|
| 510 |
self.debug_attachments(m) |
|---|
| 511 |
|
|---|
| 512 |
self.db = self.env.get_db_cnx() |
|---|
| 513 |
self.get_author_emailaddrs(m) |
|---|
| 514 |
|
|---|
| 515 |
if self.get_config('notification', 'smtp_enabled') in ['true']: |
|---|
| 516 |
self.notification = 1 |
|---|
| 517 |
else: |
|---|
| 518 |
self.notification = 0 |
|---|
| 519 |
|
|---|
| 520 |
|
|---|
| 521 |
|
|---|
| 522 |
if self.TICKET_UPDATE > 0: |
|---|
| 523 |
if self.ticket_update(m): |
|---|
| 524 |
return True |
|---|
| 525 |
|
|---|
| 526 |
self.new_ticket(m) |
|---|
| 527 |
|
|---|
| 528 |
def strip_signature(self, text): |
|---|
| 529 |
""" |
|---|
| 530 |
Strip signature from message, inspired by Mailman software |
|---|
| 531 |
""" |
|---|
| 532 |
body = [] |
|---|
| 533 |
for line in text.splitlines(): |
|---|
| 534 |
if line == '-- ': |
|---|
| 535 |
break |
|---|
| 536 |
body.append(line) |
|---|
| 537 |
|
|---|
| 538 |
return ('\n'.join(body)) |
|---|
| 539 |
|
|---|
| 540 |
def get_body_text(self, msg): |
|---|
| 541 |
""" |
|---|
| 542 |
put the message text in the ticket description or in the changes field. |
|---|
| 543 |
message text can be plain text or html or something else |
|---|
| 544 |
""" |
|---|
| 545 |
has_description = 0 |
|---|
| 546 |
encoding = True |
|---|
| 547 |
ubody_text = u'No plain text message' |
|---|
| 548 |
for part in msg.walk(): |
|---|
| 549 |
|
|---|
| 550 |
|
|---|
| 551 |
|
|---|
| 552 |
if part.get_content_maintype() == 'multipart': |
|---|
| 553 |
continue |
|---|
| 554 |
|
|---|
| 555 |
if part.get_content_type() == 'text/plain': |
|---|
| 556 |
|
|---|
| 557 |
|
|---|
| 558 |
body_text = part.get_payload(decode=1) |
|---|
| 559 |
if not body_text: |
|---|
| 560 |
body_text = part.get_payload(decode=0) |
|---|
| 561 |
|
|---|
| 562 |
if self.STRIP_SIGNATURE: |
|---|
| 563 |
body_text = self.strip_signature(body_text) |
|---|
| 564 |
|
|---|
| 565 |
|
|---|
| 566 |
|
|---|
| 567 |
charset = part.get_content_charset() |
|---|
| 568 |
if not charset: |
|---|
| 569 |
charset = 'iso-8859-15' |
|---|
| 570 |
|
|---|
| 571 |
try: |
|---|
| 572 |
ubody_text = unicode(body_text, charset) |
|---|
| 573 |
|
|---|
| 574 |
except UnicodeError, detail: |
|---|
| 575 |
ubody_text = unicode(body_text, 'iso-8859-15') |
|---|
| 576 |
|
|---|
| 577 |
except LookupError, detail: |
|---|
| 578 |
ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset) |
|---|
| 579 |
|
|---|
| 580 |
elif part.get_content_type() == 'text/html': |
|---|
| 581 |
ubody_text = '(see attachment for HTML mail message)' |
|---|
| 582 |
|
|---|
| 583 |
else: |
|---|
| 584 |
ubody_text = '(see attachment for message)' |
|---|
| 585 |
|
|---|
| 586 |
has_description = 1 |
|---|
| 587 |
break |
|---|
| 588 |
|
|---|
| 589 |
if not has_description: |
|---|
| 590 |
ubody_text = '(see attachment for message)' |
|---|
| 591 |
|
|---|
| 592 |
|
|---|
| 593 |
|
|---|
| 594 |
|
|---|
| 595 |
ubody_text = ('\r\n'.join(ubody_text.splitlines())) |
|---|
| 596 |
|
|---|
| 597 |
|
|---|
| 598 |
|
|---|
| 599 |
|
|---|
| 600 |
|
|---|
| 601 |
|
|---|
| 602 |
|
|---|
| 603 |
if self.VERBATIM_FORMAT: |
|---|
| 604 |
ubody_text = '{{{\r\n%s\r\n}}}' %ubody_text |
|---|
| 605 |
else: |
|---|
| 606 |
ubody_text = '%s' %ubody_text |
|---|
| 607 |
|
|---|
| 608 |
return ubody_text |
|---|
| 609 |
|
|---|
| 610 |
def notify(self, tkt , new=True, modtime=0): |
|---|
| 611 |
""" |
|---|
| 612 |
A wrapper for the TRAC notify function. So we can use templates |
|---|
| 613 |
""" |
|---|
| 614 |
if tkt['component'] == 'Spam': |
|---|
| 615 |
return |
|---|
| 616 |
|
|---|
| 617 |
try: |
|---|
| 618 |
|
|---|
| 619 |
|
|---|
| 620 |
self.env.abs_href = Href(self.get_config('project', 'url')) |
|---|
| 621 |
self.env.href = Href(self.get_config('project', 'url')) |
|---|
| 622 |
|
|---|
| 623 |
tn = TicketNotifyEmail(self.env) |
|---|
| 624 |
if self.notify_template: |
|---|
| 625 |
tn.template_name = self.notify_template; |
|---|
| 626 |
|
|---|
| 627 |
tn.notify(tkt, new, modtime) |
|---|
| 628 |
|
|---|
| 629 |
except Exception, e: |
|---|
| 630 |
print 'TD: Failure sending notification on creation of ticket #%s: %s' %(tkt['id'], e) |
|---|
| 631 |
|
|---|
| 632 |
def mail_line(self, str): |
|---|
| 633 |
return '%s %s' % (self.comment, str) |
|---|
| 634 |
|
|---|
| 635 |
|
|---|
| 636 |
def html_mailto_link(self, subject, id, body): |
|---|
| 637 |
if not self.author: |
|---|
| 638 |
author = self.email_addr |
|---|
| 639 |
else: |
|---|
| 640 |
author = self.author |
|---|
| 641 |
|
|---|
| 642 |
|
|---|
| 643 |
|
|---|
| 644 |
|
|---|
| 645 |
|
|---|
| 646 |
|
|---|
| 647 |
|
|---|
| 648 |
|
|---|
| 649 |
|
|---|
| 650 |
|
|---|
| 651 |
str = 'mailto:%s?Subject=%s&Cc=%s' %( |
|---|
| 652 |
urllib.quote(self.email_addr), |
|---|
| 653 |
urllib.quote('Re: #%s: %s' %(id, subject)), |
|---|
| 654 |
urllib.quote(self.MAILTO_CC) |
|---|
| 655 |
) |
|---|
| 656 |
|
|---|
| 657 |
str = '\r\n{{{\r\n#!html\r\n<a href="%s">Reply to: %s</a>\r\n}}}\r\n' %(str, author) |
|---|
| 658 |
return str |
|---|
| 659 |
|
|---|
| 660 |
def attachments(self, message, ticket, update=False): |
|---|
| 661 |
''' |
|---|
| 662 |
save any attachments as files in the ticket's directory |
|---|
| 663 |
''' |
|---|
| 664 |
count = 0 |
|---|
| 665 |
first = 0 |
|---|
| 666 |
number = 0 |
|---|
| 667 |
for part in message.walk(): |
|---|
| 668 |
if part.get_content_maintype() == 'multipart': |
|---|
| 669 |
continue |
|---|
| 670 |
|
|---|
| 671 |
if not first: |
|---|
| 672 |
first = 1 |
|---|
| 673 |
if part.get_content_type() == 'text/plain': |
|---|
| 674 |
continue |
|---|
| 675 |
|
|---|
| 676 |
filename = part.get_filename() |
|---|
| 677 |
count = count + 1 |
|---|
| 678 |
if not filename: |
|---|
| 679 |
number = number + 1 |
|---|
| 680 |
filename = 'part%04d' % number |
|---|
| 681 |
|
|---|
| 682 |
ext = mimetypes.guess_extension(part.get_content_type()) |
|---|
| 683 |
if not ext: |
|---|
| 684 |
ext = '.bin' |
|---|
| 685 |
|
|---|
| 686 |
filename = '%s%s' % (filename, ext) |
|---|
| 687 |
else: |
|---|
| 688 |
filename = self.email_to_unicode(filename) |
|---|
| 689 |
|
|---|
| 690 |
|
|---|
| 691 |
|
|---|
| 692 |
filename = filename.replace('\\', '/').replace(':', '/') |
|---|
| 693 |
filename = os.path.basename(filename) |
|---|
| 694 |
|
|---|
| 695 |
|
|---|
| 696 |
|
|---|
| 697 |
|
|---|
| 698 |
|
|---|
| 699 |
if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3): |
|---|
| 700 |
try: |
|---|
| 701 |
filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') |
|---|
| 702 |
except TypeError: |
|---|
| 703 |
pass |
|---|
| 704 |
|
|---|
| 705 |
url_filename = urllib.quote(filename) |
|---|
| 706 |
if self.VERSION == 0.8: |
|---|
| 707 |
dir = os.path.join(self.env.get_attachments_dir(), 'ticket', |
|---|
| 708 |
urllib.quote(str(ticket['id']))) |
|---|
| 709 |
if not os.path.exists(dir): |
|---|
| 710 |
mkdir_p(dir, 0755) |
|---|
| 711 |
else: |
|---|
| 712 |
dir = '/tmp' |
|---|
| 713 |
|
|---|
| 714 |
path, fd = util.create_unique_file(os.path.join(dir, url_filename)) |
|---|
| 715 |
text = part.get_payload(decode=1) |
|---|
| 716 |
if not text: |
|---|
| 717 |
text = '(None)' |
|---|
| 718 |
fd.write(text) |
|---|
| 719 |
fd.close() |
|---|
| 720 |
|
|---|
| 721 |
|
|---|
| 722 |
|
|---|
| 723 |
stats = os.lstat(path) |
|---|
| 724 |
filesize = stats[stat.ST_SIZE] |
|---|
| 725 |
|
|---|
| 726 |
|
|---|
| 727 |
|
|---|
| 728 |
if self.VERSION == 0.8: |
|---|
| 729 |
cursor = self.db.cursor() |
|---|
| 730 |
try: |
|---|
| 731 |
cursor.execute('INSERT INTO attachment VALUES("%s","%s","%s",%d,%d,"%s","%s","%s")' |
|---|
| 732 |
%('ticket', urllib.quote(str(ticket['id'])), filename + '?format=raw', filesize, |
|---|
| 733 |
int(time.time()),'', self.author, 'e-mail') ) |
|---|
| 734 |
|
|---|
| 735 |
|
|---|
| 736 |
|
|---|
| 737 |
except sqlite.IntegrityError: |
|---|
| 738 |
|
|---|
| 739 |
return count |
|---|
| 740 |
|
|---|
| 741 |
self.db.commit() |
|---|
| 742 |
|
|---|
| 743 |
else: |
|---|
| 744 |
fd = open(path) |
|---|
| 745 |
att = attachment.Attachment(self.env, 'ticket', ticket['id']) |
|---|
| 746 |
|
|---|
| 747 |
|
|---|
| 748 |
|
|---|
| 749 |
|
|---|
| 750 |
if not update: |
|---|
| 751 |
att.author = self.author |
|---|
| 752 |
att.description = self.email_to_unicode('Added by email2trac') |
|---|
| 753 |
|
|---|
| 754 |
att.insert(url_filename, fd, filesize) |
|---|
| 755 |
fd.close() |
|---|
| 756 |
|
|---|
| 757 |
|
|---|
| 758 |
|
|---|
| 759 |
os.unlink(path) |
|---|
| 760 |
|
|---|
| 761 |
|
|---|
| 762 |
|
|---|
| 763 |
return count |
|---|
| 764 |
|
|---|
| 765 |
|
|---|
| 766 |
def mkdir_p(dir, mode): |
|---|
| 767 |
'''do a mkdir -p''' |
|---|
| 768 |
|
|---|
| 769 |
arr = string.split(dir, '/') |
|---|
| 770 |
path = '' |
|---|
| 771 |
for part in arr: |
|---|
| 772 |
path = '%s/%s' % (path, part) |
|---|
| 773 |
try: |
|---|
| 774 |
stats = os.stat(path) |
|---|
| 775 |
except OSError: |
|---|
| 776 |
os.mkdir(path, mode) |
|---|
| 777 |
|
|---|
| 778 |
|
|---|
| 779 |
def ReadConfig(file, name): |
|---|
| 780 |
""" |
|---|
| 781 |
Parse the config file |
|---|
| 782 |
""" |
|---|
| 783 |
|
|---|
| 784 |
if not os.path.isfile(file): |
|---|
| 785 |
print 'File %s does not exist' %file |
|---|
| 786 |
sys.exit(1) |
|---|
| 787 |
|
|---|
| 788 |
config = ConfigParser.ConfigParser() |
|---|
| 789 |
try: |
|---|
| 790 |
config.read(file) |
|---|
| 791 |
except ConfigParser.MissingSectionHeaderError,detail: |
|---|
| 792 |
print detail |
|---|
| 793 |
sys.exit(1) |
|---|
| 794 |
|
|---|
| 795 |
|
|---|
| 796 |
|
|---|
| 797 |
|
|---|
| 798 |
if name: |
|---|
| 799 |
if not config.has_section(name): |
|---|
| 800 |
print "Not a valid project name: %s" %name |
|---|
| 801 |
print "Valid names: %s" %config.sections() |
|---|
| 802 |
sys.exit(1) |
|---|
| 803 |
|
|---|
| 804 |
project = dict() |
|---|
| 805 |
for option in config.options(name): |
|---|
| 806 |
project[option] = config.get(name, option) |
|---|
| 807 |
|
|---|
| 808 |
else: |
|---|
| 809 |
project = config.defaults() |
|---|
| 810 |
|
|---|
| 811 |
return project |
|---|
| 812 |
|
|---|
| 813 |
|
|---|
| 814 |
if __name__ == '__main__': |
|---|
| 815 |
|
|---|
| 816 |
|
|---|
| 817 |
configfile = '/etc/email2trac.conf' |
|---|
| 818 |
project = '' |
|---|
| 819 |
component = '' |
|---|
| 820 |
ENABLE_SYSLOG = 0 |
|---|
| 821 |
|
|---|
| 822 |
try: |
|---|
| 823 |
opts, args = getopt.getopt(sys.argv[1:], 'chf:p:', ['component=','help', 'file=', 'project=']) |
|---|
| 824 |
except getopt.error,detail: |
|---|
| 825 |
print __doc__ |
|---|
| 826 |
print detail |
|---|
| 827 |
sys.exit(1) |
|---|
| 828 |
|
|---|
| 829 |
project_name = None |
|---|
| 830 |
for opt,value in opts: |
|---|
| 831 |
if opt in [ '-h', '--help']: |
|---|
| 832 |
print __doc__ |
|---|
| 833 |
sys.exit(0) |
|---|
| 834 |
elif opt in ['-c', '--component']: |
|---|
| 835 |
component = value |
|---|
| 836 |
elif opt in ['-f', '--file']: |
|---|
| 837 |
configfile = value |
|---|
| 838 |
elif opt in ['-p', '--project']: |
|---|
| 839 |
project_name = value |
|---|
| 840 |
|
|---|
| 841 |
settings = ReadConfig(configfile, project_name) |
|---|
| 842 |
if not settings.has_key('project'): |
|---|
| 843 |
print __doc__ |
|---|
| 844 |
print 'No Trac project is defined in the email2trac config file.' |
|---|
| 845 |
sys.exit(1) |
|---|
| 846 |
|
|---|
| 847 |
if component: |
|---|
| 848 |
settings['component'] = component |
|---|
| 849 |
|
|---|
| 850 |
if settings.has_key('trac_version'): |
|---|
| 851 |
version = float(settings['trac_version']) |
|---|
| 852 |
else: |
|---|
| 853 |
version = trac_default_version |
|---|
| 854 |
|
|---|
| 855 |
if settings.has_key('enable_syslog'): |
|---|
| 856 |
ENABLE_SYSLOG = float(settings['enable_syslog']) |
|---|
| 857 |
|
|---|
| 858 |
|
|---|
| 859 |
|
|---|
| 860 |
|
|---|
| 861 |
try: |
|---|
| 862 |
if version == 0.8: |
|---|
| 863 |
from trac.Environment import Environment |
|---|
| 864 |
from trac.Ticket import Ticket |
|---|
| 865 |
from trac.Ticket import TicketNotifyEmail |
|---|
| 866 |
from trac.Href import Href |
|---|
| 867 |
from trac import util |
|---|
| 868 |
import sqlite |
|---|
| 869 |
elif version == 0.9: |
|---|
| 870 |
from trac import attachment |
|---|
| 871 |
from trac.env import Environment |
|---|
| 872 |
from trac.ticket import Ticket |
|---|
| 873 |
from trac.web.href import Href |
|---|
| 874 |
from trac import util |
|---|
| 875 |
from trac.Notify import TicketNotifyEmail |
|---|
| 876 |
elif version == 0.10: |
|---|
| 877 |
from trac import attachment |
|---|
| 878 |
from trac.env import Environment |
|---|
| 879 |
from trac.ticket import Ticket |
|---|
| 880 |
from trac.web.href import Href |
|---|
| 881 |
from trac import util |
|---|
| 882 |
|
|---|
| 883 |
|
|---|
| 884 |
|
|---|
| 885 |
|
|---|
| 886 |
from trac.ticket.notification import TicketNotifyEmail |
|---|
| 887 |
|
|---|
| 888 |
env = Environment(settings['project'], create=0) |
|---|
| 889 |
tktparser = TicketEmailParser(env, settings, version) |
|---|
| 890 |
tktparser.parse(sys.stdin) |
|---|
| 891 |
|
|---|
| 892 |
|
|---|
| 893 |
|
|---|
| 894 |
|
|---|
| 895 |
except Exception, error: |
|---|
| 896 |
if ENABLE_SYSLOG: |
|---|
| 897 |
syslog.openlog('email2trac', syslog.LOG_NOWAIT) |
|---|
| 898 |
etype, evalue, etb = sys.exc_info() |
|---|
| 899 |
for e in traceback.format_exception(etype, evalue, etb): |
|---|
| 900 |
syslog.syslog(e) |
|---|
| 901 |
syslog.closelog() |
|---|
| 902 |
else: |
|---|
| 903 |
traceback.print_exc() |
|---|
| 904 |
|
|---|
| 905 |
if m: |
|---|
| 906 |
tktparser.save_email_for_debug(m, True) |
|---|
| 907 |
|
|---|
| 908 |
|
|---|