source: emailtotracscript/trunk/email2trac.py.in @ 40

Last change on this file since 40 was 40, checked in by bas, 18 years ago

EmailtoTracScript?:

email2trac.py.in:

  • Use fullname for reporter if exitst else use email addr


  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 14.5 KB
Line 
1#!@PYTHON@
2# Copyright (C) 2002
3#
4# This file is part of the email2trac utils
5#
6# This program is free software; you can redistribute it and/or modify it
7# under the terms of the GNU General Public License as published by the
8# Free Software Foundation; either version 2, or (at your option) any
9# later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
19#
20"""
21emailfilter.py -- Email tickets to Trac.
22
23A simple MTA filter to create Trac tickets from inbound emails.
24
25Copyright 2005, Daniel Lundin <daniel@edgewall.com>
26Copyright 2005, Edgewall Software
27
28Changed By: Bas van der Vlies <basv@sara.nl>
29Date      : 13 September 2005
30Descr.    : Added config file and command line options, spam level
31            detection, reply address and mailto option. Unicode support
32
33Changed By: Walter de Jong <walter@sara.nl>
34Descr.    : multipart-message code and trac attachments
35
36
37The scripts reads emails from stdin and inserts directly into a Trac database.
38MIME headers are mapped as follows:
39
40        * From:      => Reporter
41                     => CC (Optional via reply_address option)
42        * Subject:   => Summary
43        * Body       => Description
44        * Component  => Can be set to SPAM via spam_level option
45
46How to use
47----------
48 * Create an config file:
49        [DEFAULT]                        # REQUIRED
50        project      : /data/trac/test   # REQUIRED
51        debug        : 1                 # OPTIONAL, if set print some DEBUG info
52        spam_level   : 4                 # OPTIONAL, if set check for SPAM mail
53        reply_address: 1                 # OPTIONAL, if set then fill in ticket CC field
54        umask        : 022               # OPTIONAL, if set then use this umask for creation of the attachments
55        mailto_link  : 1                 # OPTIONAL, if set then [mailto:<CC>] in description
56        trac_version : 0.8               # OPTIONAL, default is 0.9
57
58        [jouvin]                         # OPTIONAL project declaration, if set both fields necessary
59        project      : /data/trac/jouvin # use -p|--project jouvin. 
60       
61 * default config file is : /etc/email2trac.conf
62
63 * Commandline opions:
64                -h | --help
65                -c <value> | --component=<value>
66                -f <config file> | --file=<config file>
67                -p <project name> | --project=<project name>
68
69SVN Info:
70        $Id: email2trac.py.in 40 2006-01-24 22:54:36Z bas $
71"""
72
73import os
74import sys
75import string
76import getopt
77import stat
78import time
79import email
80import re
81import urllib
82import unicodedata
83import ConfigParser
84from email import Header
85from stat import *
86import mimetypes
87
88trac_default_version = 0.9
89
90class TicketEmailParser(object):
91        env = None
92        comment = '> '
93   
94        def __init__(self, env, parameters, version):
95                self.env = env
96
97                # Database connection
98                #
99                self.db = None
100
101                self.VERSION = version
102                if self.VERSION > 0.8:
103                        self.new_ticket = ticket.Ticket
104                        self.get_config = self.env.config.get
105                else:
106                        self.new_ticket = Ticket.Ticket
107                        self.get_config = self.env.get_config
108
109                if parameters.has_key('umask'):
110                        os.umask(int(parameters['umask'], 8))
111
112                if parameters.has_key('debug'):
113                        self.DEBUG = int(parameters['debug'])
114                else:
115                        self.DEBUG = 0
116
117                if parameters.has_key('reply_address'):
118                        self.CC = int(parameters['reply_address'])
119                else:
120                        self.CC = 0
121
122                if parameters.has_key('mailto_link'):
123                        self.MAILTO = int(parameters['mailto_link'])
124                else:
125                        self.MAILTO = 0
126
127                if parameters.has_key('spam_level'):
128                        self.SPAM_LEVEL = int(parameters['spam_level'])
129                else:
130                        self.SPAM_LEVEL = 0
131
132                if parameters.has_key('email_comment'):
133                        self.comment = str(parameters['email_comment'])
134
135                if parameters.has_key('email_header'):
136                        self.EMAIL_HEADER = int(parameters['email_header'])
137                else:
138                        self.EMAIL_HEADER = 0
139
140
141        # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
142        # Note if Spam_level then '*' are included
143        def spam(self, message):
144                if message.has_key('X-Spam-Score'):
145                        spam_l = string.split(message['X-Spam-Score'])
146                        number = spam_l[0].count('*')
147
148                        if number >= self.SPAM_LEVEL:
149                                return number
150
151                return 0
152
153        def to_unicode(self, str):
154                """
155                Email has 7 bit ASCII code, convert it to unicode with the charset
156                that is encoded in 7-bit ASCII code and encode it as utf-8 so TRAC
157                understands it.
158                """
159                results =  Header.decode_header(str)
160                str = None
161                for text,format in results:
162                        if format:
163                                try:
164                                        temp = unicode(text, format)
165                                except UnicodeError:
166                                        # This always works
167                                        #
168                                        temp = unicode(text, 'iso-8859-15')
169                                temp =  temp.encode('utf-8')
170                        else:
171                                temp = string.strip(text)
172
173                        if str:
174                                str = '%s %s' %(str, temp)
175                        else:
176                                str = temp
177
178                return str
179
180        def debug_attachments(self, message):
181                n = 0
182                for part in message.walk():
183                        if part.get_content_maintype() == 'multipart':      # multipart/* is just a container
184                                print 'TD: multipart container'
185                                continue
186
187                        n = n + 1
188                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
189                        print 'TD: part%d: filename: %s' % (n, part.get_filename())
190
191                        if part.is_multipart():
192                                print 'TD: this part is multipart'
193                                payload = part.get_payload(decode=1)
194                                print 'TD: payload:', payload
195                        else:
196                                print 'TD: this part is not multipart'
197
198                        part_file = '/var/tmp/part%d' % n
199                        print 'TD: writing part%d (%s)' % (n,part_file)
200                        fx = open(part_file, 'wb')
201                        text = part.get_payload(decode=1)
202                        if not text:
203                                text = '(None)'
204                        fx.write(text)
205                        fx.close()
206                        try:
207                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
208                        except OSError:
209                                pass
210
211        def email_header_txt(self, m):
212#               if not m['Subject']:
213#                       subject = '(geen subject)'
214#               else:
215#                       subject = self.to_unicode(m['Subject'])
216#
217#               head = "'''Subject:''' %s [[BR]]" % subject
218#               if m['From'] and len(m['From']) > 0:
219#                       head = "%s'''From:''' %s [[BR]]" % (head, m['From'])
220#               if m['Date'] and len(m['Date']) > 0:
221#                       head = "%s'''Date:''' %s [[BR]]" %(head, m['Date'])
222
223                str = ''
224                if m['To'] and len(m['To']) > 0 and m['To'] != 'hic@sara.nl':
225                        str = "'''To:''' %s [[BR]]" %(m['To'])
226                if m['Cc'] and len(m['Cc']) > 0:
227                        str = "%s'''Cc:''' %s [[BR]]" % (str, m['Cc'])
228
229                return str
230
231        def parse(self, fp):
232                msg = email.message_from_file(fp)
233                if not msg:
234                        return
235
236                if self.DEBUG > 1:        # save the entire e-mail message text
237                        msg_file = '/var/tmp/msg.txt'
238                        print 'TD: saving email to %s' % msg_file
239                        fx = open(msg_file, 'wb')
240                        fx.write('%s' % msg)
241                        fx.close()
242                        try:
243                                os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
244                        except OSError:
245                                pass
246
247                self.db = self.env.get_db_cnx()
248                tkt = self.new_ticket(self.env)
249                tkt['status'] = 'new'
250
251                # Some defaults
252                #
253                tkt['milestone'] = self.get_config('ticket', 'default_milestone')
254                tkt['priority'] = self.get_config('ticket', 'default_priority')
255                tkt['severity'] = self.get_config('ticket', 'default_severity')
256                tkt['version'] = self.get_config('ticket', 'default_version')
257
258                if not msg['Subject']:
259                        tkt['summary'] = '(geen subject)'
260                else:
261                        tkt['summary'] = self.to_unicode(msg['Subject'])
262
263
264                if self.SPAM_LEVEL:
265                        tkt['component'] = self.spam(msg)
266                elif settings.has_key('component'):
267                        tkt['component'] = settings['component']
268                else:
269                        tkt['component'] = self.get_config('ticket', 'default_component')
270
271                # Must make this an option or so, discard SPAM messages or save then
272                # and delete later
273                #
274                #if self.SPAM_LEVEL and self.spam(msg):
275                #       print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
276                #       sys.exit(1)
277
278                # Get default owner for component
279                #
280                cursor = self.db.cursor()
281                sql = 'SELECT owner FROM component WHERE name=\'%s\'' % tkt['component']
282                cursor.execute(sql)
283                tkt['owner'] = cursor.fetchone()[0]
284
285                author, email_addr  = email.Utils.parseaddr(msg['from'])
286                email_str = self.to_unicode(email_addr)
287                if author:
288                        tkt['reporter'] = self.to_unicode(author)
289                else
290                        tkt['reporter'] = self.to_unicode(email_str)
291
292                if self.CC:
293                        tkt['cc'] = email_str
294
295# produce e-mail like header
296                head = ''
297                if self.EMAIL_HEADER > 0:
298                        head = self.email_header_txt(msg)
299
300                if self.DEBUG > 0:
301                        self.debug_attachments(msg)
302
303#
304#       put the message text in the ticket description
305#       message text can be plain text or html or something else
306#
307                has_description = 0
308                for part in msg.walk():
309                        if part.get_content_maintype() == 'multipart':                  # 'multipart/*' is a container for multipart messages
310                                continue
311
312                        if part.get_content_type() == 'text/plain':
313                                body_text = part.get_payload(decode=1)                  # try to decode
314                                if not body_text:                                       # decode failed
315                                        body_text = part.get_payload(decode=0)          # do not decode
316
317                                tkt['description'] = '\n{{{\n\n%s\n}}}\n' % body_text
318
319                        elif part.get_content_type() == 'text/html':
320                                tkt['description'] = '%s\n\n(see attachment for HTML mail message)\n' % head
321                                body_text = tkt['description']
322
323                        else:
324                                tkt['description'] = '%s\n\n(see attachment for message)\n' % head
325                                body_text = tkt['description']
326
327                        has_description = 1
328                        break           # we have the description, so break
329
330                if not has_description:
331                        tkt['description'] = '%s\n\n(no plain text message, see attachments)' % head
332                        has_description = 1
333
334                if self.MAILTO:
335                        mailto = self.html_mailto_link(author, email_addr, self.to_unicode(msg['subject']), body_text)
336                        tkt['description'] = '%s\n%s %s' %(head, mailto, tkt['description'])
337
338                if self.VERSION > 0.8:
339                        tkt['id'] = tkt.insert()
340                else:
341                        tkt['id'] = tkt.insert(self.db)
342
343                #
344                # Just how to show to update description
345                #
346                #tkt['description'] = '\n{{{\n\n Bas is op nieuw bezig\n\n }}}\n'
347                #tkt.save_changes(self.db, author, "Lekker bezig")
348                #
349                self.attachments(msg, tkt, author)
350
351
352        def mail_line(self, str):
353                return '%s %s' % (self.comment, str)
354
355
356        def html_mailto_link(self, author, mail_addr, subject, body):
357                if not author:
358                        author = mail_addr
359                else:   
360                        author = self.to_unicode(author)
361
362                # Must find a fix
363                #
364                #arr = string.split(body, '\n')
365                #arr = map(self.mail_line, arr)
366
367                #body = string.join(arr, '\n')
368                #body = '%s wrote:\n%s' %(author, body)
369
370                # Obsolete for reference
371                #
372                #body = self.to_unicode(body)
373                #body = urllib.quote(body)
374                #body = Header.encode(body)
375                #
376
377                # Temporary fix
378                body = '> Type your reply'
379                str = 'mailto:%s?subject=%s&body=%s' % (urllib.quote(mail_addr), urllib.quote('Re: %s' % subject), urllib.quote(body))
380                str = '\n{{{\n#!html\n<a href="%s">Reply to: %s</a>\n}}}\n' %(str, author)
381
382                return str
383
384        def attachments(self, message, ticket, user):
385                '''save any attachments as file in the ticket's directory'''
386
387                count = 0
388                first = 0
389                for part in message.walk():
390                        if part.get_content_maintype() == 'multipart':          # multipart/* is just a container
391                                continue
392
393                        if not first:                                                                           # first content is the message
394                                first = 1
395                                if part.get_content_type() == 'text/plain':             # if first is text, is was already put in the description
396                                        continue
397
398                        filename = part.get_filename()
399                        if not filename:
400                                count = count + 1
401                                filename = 'part%04d' % count
402
403                                ext = mimetypes.guess_extension(part.get_type())
404                                if not ext:
405                                        ext = '.bin'
406
407                                filename = '%s%s' % (filename, ext)
408                        else:
409                                filename = self.to_unicode(filename)
410
411                        if '/' in filename:
412                                filename = os.path.basename(filename)
413
414                        url_filename = urllib.quote(filename)
415
416                        if self.VERSION > 0.8:
417                                tmpfile = '/tmp/email2trac-ticket%sattachment' % str(ticket['id'])
418                        else:
419                                dir = os.path.join(self.env.get_attachments_dir(), 'ticket',
420                                                        urllib.quote(str(ticket['id'])))
421                                if not os.path.exists(dir):
422                                        mkdir_p(dir, 0755)
423
424                                tmpfile = os.path.join(dir, url_filename)
425
426                        f = open(tmpfile, 'wb')
427                        text = part.get_payload(decode=1)
428                        if not text:
429                                text = '(None)'
430                        f.write(text)
431
432                        # get the filesize
433                        #
434                        stats = os.lstat(tmpfile)
435                        filesize = stats[stat.ST_SIZE]
436
437                        # Insert the attachment it differs for the different TRAC versions
438                        #
439                        if self.VERSION > 0.8:
440                                att = attachment.Attachment(self.env,'ticket',ticket['id'])
441                                att.insert(url_filename,f,filesize)
442                                f.close()
443                        else:
444                                cursor = self.db.cursor()
445                                cursor.execute('INSERT INTO attachment VALUES("%s","%s","%s",%d,%d,"%s","%s","%s")'
446                                        %('ticket', urllib.quote(str(ticket['id'])), filename + '?format=raw', filesize,
447                                          int(time.time()),'', user, 'e-mail') )
448                                self.db.commit()
449
450
451def mkdir_p(dir, mode):
452        '''do a mkdir -p'''
453
454        arr = string.split(dir, '/')
455        path = ''
456        for part in arr:
457                path = '%s/%s' % (path, part)
458                try:
459                        stats = os.stat(path)
460                except OSError:
461                        os.mkdir(path, mode)
462
463
464def ReadConfig(file, name):
465        """
466        Parse the config file
467        """
468
469        if not os.path.isfile(file):
470                print 'File %s does not exists' %file
471                sys.exit(1)
472
473        config = ConfigParser.ConfigParser()
474        try:
475                config.read(file)
476        except ConfigParser.MissingSectionHeaderError,detail:
477                print detail
478                sys.exit(1)
479
480
481        # Use given project name else use defaults
482        #
483        if name:
484                if not config.has_section(name):
485                        print "Not an valid project name: %s" %name
486                        print "Valid names: %s" %config.sections()
487                        sys.exit(1)
488
489                project =  dict()
490                for option in  config.options(name):
491                        project[option] = config.get(name, option)
492
493        else:
494                project = config.defaults()
495
496        return project
497
498if __name__ == '__main__':
499        # Default config file
500        #
501        configfile = '@email2trac_conf@'
502        project = ''
503        component = ''
504       
505        try:
506                opts, args = getopt.getopt(sys.argv[1:], 'chf:p:', ['component=','help', 'file=', 'project='])
507        except getopt.error,detail:
508                print __doc__
509                print detail
510                sys.exit(1)
511
512        project_name = None
513        for opt,value in opts:
514                if opt in [ '-h', '--help']:
515                        print __doc__
516                        sys.exit(0)
517                elif opt in ['-c', '--component']:
518                        component = value
519                elif opt in ['-f', '--file']:
520                        configfile = value
521                elif opt in ['-p', '--project']:
522                        project_name = value
523
524        settings = ReadConfig(configfile, project_name)
525        if not settings.has_key('project'):
526                print __doc__
527                print 'No project defined in config file, eg:\n\t project: /data/trac/bas'
528                sys.exit(1)
529
530        if component:
531                settings['component'] = component
532
533        if settings.has_key('trac_version'):
534                version = float(settings['trac_version'])
535        else:
536                version = trac_default_version
537
538        #debug HvB
539        #print settings
540
541        if version > 0.8:
542                from trac import attachment, config, env, ticket
543                env = env.Environment(settings['project'], create=0)
544                ticket_mod = ticket
545        else:
546                from trac import Environment, Ticket
547                env = Environment.Environment(settings['project'], create=0)
548                ticket_mod = Ticket
549               
550        tktparser = TicketEmailParser(env, settings, version)
551        tktparser.parse(sys.stdin)
552
553# EOB
Note: See TracBrowser for help on using the repository browser.