Commit 3f38d993 authored by nextime's avatar nextime

first commit

parents
[DEFAULT]
project: /data/trac/test
debug: 2
umask: 022
reply_all : 1
mailto_link: 0
umask: 022
email_comment: >
email_header: 0
trac_version: 0.10
enable_syslog : 0
alternate_notify_template :
drop_spam : 0
verbatim_format: 1
strip_signature: 1
myaddress: ticket@trac.tld
[bas]
project: /data/trac/bas
myaddress: bas@ticket.address.tld
#!/usr/bin/env python
# Copyright (C) 2002
#
# This file is part of the email2trac utils
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2, or (at your option) any
# later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
#
# For vi/emacs or other use tabstop=4 (vi: set ts=4)
#
"""
email2trac.py -- Email tickets to Trac.
A simple MTA filter to create Trac tickets from inbound emails.
Copyright 2005, Daniel Lundin <daniel@edgewall.com>
Copyright 2005, Edgewall Software
Changed By: Bas van der Vlies <basv@sara.nl>
Date : 13 September 2005
Descr. : Added config file and command line options, spam level
detection, reply address and mailto option. Unicode support
Changed By: Walter de Jong <walter@sara.nl>
Descr. : multipart-message code and trac attachments
Changed By: Franco (nextime) Lanza <nextime@nexlab.it>
Date: 18/01/2007
Descr. : Some semplifications on the script.
The scripts reads emails from stdin and inserts directly into a Trac database.
MIME headers are mapped as follows:
* From: => Reporter
=> CC (Optional via reply_address option)
* Subject: => Summary
* Body => Description
* Component => Can be set to SPAM via spam_level option
How to use
----------
* Create an config file:
[DEFAULT] # REQUIRED
project : /data/trac/test # REQUIRED
debug : 1 # OPTIONAL, if set print some DEBUG info
reply_address: 1 # OPTIONAL, if set then fill in ticket CC field
umask : 022 # OPTIONAL, if set then use this umask for creation of the attachments
mailto_link : 1 # OPTIONAL, if set then [mailto:<>] in description
myaddress : address@trac.tld
mailto_cc : basv@sara.nl # OPTIONAL, use this address as CC in mailto line
ticket_update: 1 # OPTIONAL, if set then check if this is an update for a ticket
trac_version : 0.9 # OPTIONAL, default is 0.10
[jouvin] # OPTIONAL project declaration, if set both fields necessary
project : /data/trac/jouvin # use -p|--project jouvin.
myaddress : ticket@jouin.tld
* default config file is : /etc/email2trac.conf
* Commandline opions:
-h | --help
-c <value> | --component=<value>
-f <config file> | --file=<config file>
-p <project name> | --project=<project name>
"""
import os
import sys
import string
import getopt
import stat
import time
import email
import email.Iterators
import email.Header
import re
import urllib
import unicodedata
import ConfigParser
from stat import *
import mimetypes
import syslog
import traceback
# Some global variables
#
trac_default_version = 0.10
m = None
class TicketEmailParser(object):
env = None
comment = '> '
def __init__(self, env, parameters, version):
self.env = env
# Database connection
#
self.db = None
# Some useful mail constants
#
self.author = None
self.email_addr = None
self.email_field = None
self.VERSION = version
if self.VERSION == 0.8:
self.get_config = self.env.get_config
else:
self.get_config = self.env.config.get
if parameters.has_key('umask'):
os.umask(int(parameters['umask'], 8))
if parameters.has_key('myaddress'):
self.myaddress = parameters['myaddress']
else:
self.myaddress = None
if parameters.has_key('debug'):
self.DEBUG = int(parameters['debug'])
else:
self.DEBUG = 0
if parameters.has_key('mailto_link'):
self.MAILTO = int(parameters['mailto_link'])
if parameters.has_key('mailto_cc'):
self.MAILTO_CC = parameters['mailto_cc']
else:
self.MAILTO_CC = ''
else:
self.MAILTO = 0
if parameters.has_key('email_comment'):
self.comment = str(parameters['email_comment'])
if parameters.has_key('email_header'):
self.EMAIL_HEADER = int(parameters['email_header'])
else:
self.EMAIL_HEADER = 0
if parameters.has_key('alternate_notify_template'):
self.notify_template = str(parameters['alternate_notify_template'])
else:
self.notify_template = None
if parameters.has_key('reply_all'):
self.REPLY_ALL = int(parameters['reply_all'])
else:
self.REPLY_ALL = 0
if parameters.has_key('ticket_update'):
self.TICKET_UPDATE = int(parameters['ticket_update'])
else:
self.TICKET_UPDATE = 0
if parameters.has_key('drop_spam'):
self.DROP_SPAM = int(parameters['drop_spam'])
else:
self.DROP_SPAM = 0
if parameters.has_key('verbatim_format'):
self.VERBATIM_FORMAT = int(parameters['verbatim_format'])
else:
self.VERBATIM_FORMAT = 1
if parameters.has_key('strip_signature'):
self.STRIP_SIGNATURE = int(parameters['strip_signature'])
else:
self.STRIP_SIGNATURE = 0
def spam(self, message):
if message.has_key('X-Spam-Status'):
spamvalue = message['X-Spam-Status'].split(",")[0]
if spamvalue == "Yes":
return 'Spam'
elif message.has_key('X-Virus-found'): # treat virus mails as spam
return 'Spam'
return self.get_config('ticket', 'default_component')
def email_to_unicode(self, message_str):
"""
Email has 7 bit ASCII code, convert it to unicode with the charset
that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
understands it.
"""
results = email.Header.decode_header(message_str)
str = None
for text,format in results:
if format:
try:
temp = unicode(text, format)
except UnicodeError, detail:
# This always works
#
temp = unicode(text, 'iso-8859-15')
except LookupError, detail:
#text = 'ERROR: Could not find charset: %s, please install' %format
#temp = unicode(text, 'iso-8859-15')
temp = message_str
else:
temp = string.strip(text)
temp = unicode(text, 'iso-8859-15')
if str:
str = '%s %s' %(str, temp)
else:
str = '%s' %temp
#str = str.encode('utf-8')
return str
def debug_attachments(self, message):
n = 0
for part in message.walk():
if part.get_content_maintype() == 'multipart': # multipart/* is just a container
print 'TD: multipart container'
continue
n = n + 1
print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
print 'TD: part%d: filename: %s' % (n, part.get_filename())
if part.is_multipart():
print 'TD: this part is multipart'
payload = part.get_payload(decode=1)
print 'TD: payload:', payload
else:
print 'TD: this part is not multipart'
part_file = '/tmp/part%d' % n
print 'TD: writing part%d (%s)' % (n,part_file)
fx = open(part_file, 'wb')
text = part.get_payload(decode=1)
if not text:
text = '(None)'
fx.write(text)
fx.close()
try:
os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
except OSError:
pass
def email_header_txt(self, m):
"""
Display To and CC addresses in description field
"""
str = ''
if m['To'] and len(m['To']) > 0 and m['To'] != 'hic@sara.nl':
str = "'''To:''' %s [[BR]]" %(m['To'])
if m['Cc'] and len(m['Cc']) > 0:
str = "%s'''Cc:''' %s [[BR]]" % (str, m['Cc'])
return self.email_to_unicode(str)
def set_owner(self, ticket):
"""
Select default owner for ticket component
"""
cursor = self.db.cursor()
sql = "SELECT owner FROM component WHERE name='%s'" % ticket['component']
cursor.execute(sql)
try:
ticket['owner'] = cursor.fetchone()[0]
except TypeError, detail:
ticket['owner'] = "UNKNOWN"
def get_author_emailaddrs(self, message):
"""
Get the default author name and email address from the message
"""
temp = self.email_to_unicode(message['from'])
#print temp.encode('utf-8')
self.author, self.email_addr = email.Utils.parseaddr(temp)
# if the "from" email addr is equal to the trac sender,
# exit and do nothing!
if self.email_addr == self.myaddress:
sys.exit(0)
#print self.author.encode('utf-8', 'replace')
# Look for email address in registered trac users
#
if self.VERSION == 0.8:
users = []
else:
users = [ u for (u, n, e) in self.env.get_known_users(self.db)
if e == self.email_addr ]
if len(users) == 1:
self.email_field = users[0]
else:
self.email_field = self.email_to_unicode(message['from'])
def set_reply_fields(self, ticket, message):
"""
Set all the right fields for a new ticket
"""
ticket['reporter'] = self.email_field
# Put all CC-addresses in ticket CC field
#
if self.REPLY_ALL:
#tos = message.get_all('to', [])
ccs = message.get_all('cc', [])
addrs = email.Utils.getaddresses(ccs)
if not addrs:
return
# Remove reporter email address if notification is
# on
#
if self.notification:
try:
addrs.remove((self.author, self.email_addr))
except ValueError, detail:
pass
for name,mail in addrs:
# Remove the trac email2ticket address,
# and add others to mail_list
if mail != self.myaddress:
try:
mail_list = '%s, %s' %(mail_list, mail)
except:
mail_list = mail
if mail_list:
ticket['cc'] = self.email_to_unicode(mail_list)
def save_email_for_debug(self, message, tempfile=False):
if tempfile:
import tempfile
msg_file = tempfile.mktemp('.email2trac')
else:
msg_file = '/tmp/msg.txt'
print 'TD: saving email to %s' % msg_file
fx = open(msg_file, 'wb')
fx.write('%s' % message)
fx.close()
try:
os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
except OSError:
pass
def ticket_update(self, m):
"""
If the current email is a reply to an existing ticket, this function
will append the contents of this email to that ticket, instead of
creating a new one.
"""
if not m['Subject']:
return False
else:
subject = self.email_to_unicode(m['Subject'])
TICKET_RE = re.compile(r"""
(?P<ticketnr>[#][0-9]+:)
""", re.VERBOSE)
result = TICKET_RE.search(subject)
if not result:
return False
body_text = self.get_body_text(m)
# Strip '#' and ':' from ticket_id
#
ticket_id = result.group('ticketnr')
ticket_id = int(ticket_id[1:-1])
# Get current time
#
when = int(time.time())
if self.VERSION == 0.8:
tkt = Ticket(self.db, ticket_id)
tkt.save_changes(self.db, self.author, body_text, when)
else:
try:
tkt = Ticket(self.env, ticket_id, self.db)
except util.TracError, detail:
return False
tkt.save_changes(self.author, body_text, when)
tkt['id'] = ticket_id
if (self.VERSION == 0.9) or (self.VERSION == 0.10):
self.attachments(m, tkt, True)
else:
self.attachments(m, tkt)
if self.notification:
self.notify(tkt, False, when)
return True
def new_ticket(self, msg):
"""
Create a new ticket
"""
tkt = Ticket(self.env)
tkt['status'] = 'new'
# Some defaults
#
tkt['milestone'] = self.get_config('ticket', 'default_milestone')
tkt['priority'] = self.get_config('ticket', 'default_priority')
tkt['severity'] = self.get_config('ticket', 'default_severity')
tkt['version'] = self.get_config('ticket', 'default_version')
if not msg['Subject']:
tkt['summary'] = u'(geen subject)'
else:
tkt['summary'] = self.email_to_unicode(msg['Subject'])
if settings.has_key('component'):
tkt['component'] = settings['component']
else:
tkt['component'] = self.spam(msg)
# Discard SPAM messages.
#
if self.DROP_SPAM and (tkt['component'] == 'Spam'):
# print 'This message is a SPAM. Automatic ticket insertion refused
return False
# Set default owner for component
#
self.set_owner(tkt)
self.set_reply_fields(tkt, msg)
# produce e-mail like header
#
head = ''
if self.EMAIL_HEADER > 0:
head = self.email_header_txt(msg)
body_text = self.get_body_text(msg)
tkt['description'] = '\r\n%s\r\n%s' \
%(head, body_text)
when = int(time.time())
if self.VERSION == 0.8:
ticket_id = tkt.insert(self.db)
else:
ticket_id = tkt.insert()
tkt['id'] = ticket_id
changed = False
comment = ''
# Rewrite the description if we have mailto enabled
#
if self.MAILTO:
changed = True
comment = u'\nadded mailto line\n'
mailto = self.html_mailto_link(tkt['summary'], ticket_id, body_text)
tkt['description'] = u'\r\n%s\r\n%s%s\r\n' \
%(head, mailto, body_text)
n = self.attachments(msg, tkt)
if n:
changed = True
comment = '%s\nThis message has %d attachment(s)\n' %(comment, n)
if changed:
if self.VERSION == 0.8:
tkt.save_changes(self.db, self.author, comment)
else:
tkt.save_changes(self.author, comment)
#print tkt.get_changelog(self.db, when)
if self.notification:
self.notify(tkt, True)
#self.notify(tkt, False)
def parse(self, fp):
global m
m = email.message_from_file(fp)
if not m:
return
if self.DEBUG > 1: # save the entire e-mail message text
self.save_email_for_debug(m)
self.debug_attachments(m)
self.db = self.env.get_db_cnx()
self.get_author_emailaddrs(m)
if self.get_config('notification', 'smtp_enabled') in ['true']:
self.notification = 1
else:
self.notification = 0
# Must we update existing tickets
#
if self.TICKET_UPDATE > 0:
if self.ticket_update(m):
return True
self.new_ticket(m)
def strip_signature(self, text):
"""
Strip signature from message, inspired by Mailman software
"""
body = []
for line in text.splitlines():
if line == '-- ':
break
body.append(line)
return ('\n'.join(body))
def get_body_text(self, msg):
"""
put the message text in the ticket description or in the changes field.
message text can be plain text or html or something else
"""
has_description = 0
encoding = True
ubody_text = u'No plain text message'
for part in msg.walk():
# 'multipart/*' is a container for multipart messages
#
if part.get_content_maintype() == 'multipart':
continue
if part.get_content_type() == 'text/plain':
# Try to decode, if fails then do not decode
#
body_text = part.get_payload(decode=1)
if not body_text:
body_text = part.get_payload(decode=0)
if self.STRIP_SIGNATURE:
body_text = self.strip_signature(body_text)
# Get contents charset (iso-8859-15 if not defined in mail headers)
#
charset = part.get_content_charset()
if not charset:
charset = 'iso-8859-15'
try:
ubody_text = unicode(body_text, charset)
except UnicodeError, detail:
ubody_text = unicode(body_text, 'iso-8859-15')
except LookupError, detail:
ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
elif part.get_content_type() == 'text/html':
ubody_text = '(see attachment for HTML mail message)'
else:
ubody_text = '(see attachment for message)'
has_description = 1
break # we have the description, so break
if not has_description:
ubody_text = '(see attachment for message)'
# A patch so that the web-interface will not update the description
# field of a ticket
#
ubody_text = ('\r\n'.join(ubody_text.splitlines()))
# If we can unicode it try to encode it for trac
# else we a lot of garbage
#
#if encoding:
# ubody_text = ubody_text.encode('utf-8')
if self.VERBATIM_FORMAT:
ubody_text = '{{{\r\n%s\r\n}}}' %ubody_text
else:
ubody_text = '%s' %ubody_text
return ubody_text
def notify(self, tkt , new=True, modtime=0):
"""
A wrapper for the TRAC notify function. So we can use templates
"""
if tkt['component'] == 'Spam':
return
try:
# create false {abs_}href properties, to trick Notify()
#
self.env.abs_href = Href(self.get_config('project', 'url'))
self.env.href = Href(self.get_config('project', 'url'))
tn = TicketNotifyEmail(self.env)
if self.notify_template:
tn.template_name = self.notify_template;
tn.notify(tkt, new, modtime)
except Exception, e:
print 'TD: Failure sending notification on creation of ticket #%s: %s' %(tkt['id'], e)
def mail_line(self, str):
return '%s %s' % (self.comment, str)
def html_mailto_link(self, subject, id, body):
if not self.author:
author = self.email_addr
else:
author = self.author
# Must find a fix
#
#arr = string.split(body, '\n')
#arr = map(self.mail_line, arr)
#body = string.join(arr, '\n')
#body = '%s wrote:\n%s' %(author, body)
# Temporary fix
#
str = 'mailto:%s?Subject=%s&Cc=%s' %(
urllib.quote(self.email_addr),
urllib.quote('Re: #%s: %s' %(id, subject)),
urllib.quote(self.MAILTO_CC)
)
str = '\r\n{{{\r\n#!html\r\n<a href="%s">Reply to: %s</a>\r\n}}}\r\n' %(str, author)
return str
def attachments(self, message, ticket, update=False):
'''
save any attachments as files in the ticket's directory
'''
count = 0
first = 0
number = 0
for part in message.walk():
if part.get_content_maintype() == 'multipart': # multipart/* is just a container
continue
if not first: # first content is the message
first = 1
if part.get_content_type() == 'text/plain': # if first is text, is was already put in the description
continue
filename = part.get_filename()
count = count + 1
if not filename:
number = number + 1
filename = 'part%04d' % number
ext = mimetypes.guess_extension(part.get_content_type())
if not ext:
ext = '.bin'
filename = '%s%s' % (filename, ext)
else:
filename = self.email_to_unicode(filename)
# From the trac code
#
filename = filename.replace('\\', '/').replace(':', '/')
filename = os.path.basename(filename)
# We try to normalize the filename to utf-8 NFC if we can.
# Files uploaded from OS X might be in NFD.
# Check python version and then try it
#
if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
try:
filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8')
except TypeError:
pass
url_filename = urllib.quote(filename)
if self.VERSION == 0.8:
dir = os.path.join(self.env.get_attachments_dir(), 'ticket',
urllib.quote(str(ticket['id'])))
if not os.path.exists(dir):
mkdir_p(dir, 0755)
else:
dir = '/tmp'
path, fd = util.create_unique_file(os.path.join(dir, url_filename))
text = part.get_payload(decode=1)
if not text:
text = '(None)'
fd.write(text)
fd.close()
# get the filesize
#
stats = os.lstat(path)
filesize = stats[stat.ST_SIZE]
# Insert the attachment it differs for the different TRAC versions
#
if self.VERSION == 0.8:
cursor = self.db.cursor()
try:
cursor.execute('INSERT INTO attachment VALUES("%s","%s","%s",%d,%d,"%s","%s","%s")'
%('ticket', urllib.quote(str(ticket['id'])), filename + '?format=raw', filesize,
int(time.time()),'', self.author, 'e-mail') )
# Attachment is already known
#
except sqlite.IntegrityError:
#self.db.close()
return count
self.db.commit()
else:
fd = open(path)
att = attachment.Attachment(self.env, 'ticket', ticket['id'])
# This will break the ticket_update system, the body_text is vaporized
# ;-(
#
if not update:
att.author = self.author
att.description = self.email_to_unicode('Added by email2trac')
att.insert(url_filename, fd, filesize)
fd.close()
# Remove the created temporary filename
#
os.unlink(path)
# Return how many attachments
#
return count
def mkdir_p(dir, mode):
'''do a mkdir -p'''
arr = string.split(dir, '/')
path = ''
for part in arr:
path = '%s/%s' % (path, part)
try:
stats = os.stat(path)
except OSError:
os.mkdir(path, mode)
def ReadConfig(file, name):
"""
Parse the config file
"""
if not os.path.isfile(file):
print 'File %s does not exist' %file
sys.exit(1)
config = ConfigParser.ConfigParser()
try:
config.read(file)
except ConfigParser.MissingSectionHeaderError,detail:
print detail
sys.exit(1)
# Use given project name else use defaults
#
if name:
if not config.has_section(name):
print "Not a valid project name: %s" %name
print "Valid names: %s" %config.sections()
sys.exit(1)
project = dict()
for option in config.options(name):
project[option] = config.get(name, option)
else:
project = config.defaults()
return project
if __name__ == '__main__':
# Default config file
#
configfile = '/etc/email2trac.conf'
project = ''
component = ''
ENABLE_SYSLOG = 0
try:
opts, args = getopt.getopt(sys.argv[1:], 'chf:p:', ['component=','help', 'file=', 'project='])
except getopt.error,detail:
print __doc__
print detail
sys.exit(1)
project_name = None
for opt,value in opts:
if opt in [ '-h', '--help']:
print __doc__
sys.exit(0)
elif opt in ['-c', '--component']:
component = value
elif opt in ['-f', '--file']:
configfile = value
elif opt in ['-p', '--project']:
project_name = value
settings = ReadConfig(configfile, project_name)
if not settings.has_key('project'):
print __doc__
print 'No Trac project is defined in the email2trac config file.'
sys.exit(1)
if component:
settings['component'] = component
if settings.has_key('trac_version'):
version = float(settings['trac_version'])
else:
version = trac_default_version
if settings.has_key('enable_syslog'):
ENABLE_SYSLOG = float(settings['enable_syslog'])
#debug HvB
#print settings
try:
if version == 0.8:
from trac.Environment import Environment
from trac.Ticket import Ticket
from trac.Ticket import TicketNotifyEmail
from trac.Href import Href
from trac import util
import sqlite
elif version == 0.9:
from trac import attachment
from trac.env import Environment
from trac.ticket import Ticket
from trac.web.href import Href
from trac import util
from trac.Notify import TicketNotifyEmail
elif version == 0.10:
from trac import attachment
from trac.env import Environment
from trac.ticket import Ticket
from trac.web.href import Href
from trac import util
#
# return util.text.to_unicode(str)
#
# see http://projects.edgewall.com/trac/changeset/2799
from trac.ticket.notification import TicketNotifyEmail
env = Environment(settings['project'], create=0)
tktparser = TicketEmailParser(env, settings, version)
tktparser.parse(sys.stdin)
# Catch all errors ans log to SYSLOG if we have enabled this
# else stdout
#
except Exception, error:
if ENABLE_SYSLOG:
syslog.openlog('email2trac', syslog.LOG_NOWAIT)
etype, evalue, etb = sys.exc_info()
for e in traceback.format_exception(etype, evalue, etb):
syslog.syslog(e)
syslog.closelog()
else:
traceback.print_exc()
if m:
tktparser.save_email_for_debug(m, True)
# EOB
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment