source: trunk/email2trac.py.in @ 190

Last change on this file since 190 was 190, checked in by bas, 17 years ago

email2trac.py.in:

  • Applied patch to check if syslog is available
  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 27.1 KB
RevLine 
[22]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#
[80]20# For vi/emacs or other use tabstop=4 (vi: set ts=4)
21#
[22]22"""
[54]23email2trac.py -- Email tickets to Trac.
[22]24
25A simple MTA filter to create Trac tickets from inbound emails.
26
27Copyright 2005, Daniel Lundin <daniel@edgewall.com>
28Copyright 2005, Edgewall Software
29
30Changed By: Bas van der Vlies <basv@sara.nl>
31Date      : 13 September 2005
32Descr.    : Added config file and command line options, spam level
33            detection, reply address and mailto option. Unicode support
34
35Changed By: Walter de Jong <walter@sara.nl>
36Descr.    : multipart-message code and trac attachments
37
38
39The scripts reads emails from stdin and inserts directly into a Trac database.
40MIME headers are mapped as follows:
41
42        * From:      => Reporter
[152]43                     => CC (Optional via reply_all option)
[22]44        * Subject:   => Summary
45        * Body       => Description
46        * Component  => Can be set to SPAM via spam_level option
47
48How to use
49----------
50 * Create an config file:
[74]51        [DEFAULT]                      # REQUIRED
52        project      : /data/trac/test # REQUIRED
53        debug        : 1               # OPTIONAL, if set print some DEBUG info
54        spam_level   : 4               # OPTIONAL, if set check for SPAM mail
[152]55        reply_all    : 1               # OPTIONAL, if set then fill in ticket CC field
[87]56        umask        : 022             # OPTIONAL, if set then use this umask for creation of the attachments
[74]57        mailto_link  : 1               # OPTIONAL, if set then [mailto:<>] in description
[75]58        mailto_cc    : basv@sara.nl    # OPTIONAL, use this address as CC in mailto line
[74]59        ticket_update: 1               # OPTIONAL, if set then check if this is an update for a ticket
[172]60        trac_version : 0.9             # OPTIONAL, default is 0.10
[22]61
[148]62        [jouvin]                       # OPTIONAL project declaration, if set both fields necessary
[22]63        project      : /data/trac/jouvin # use -p|--project jouvin. 
64       
65 * default config file is : /etc/email2trac.conf
66
67 * Commandline opions:
68                -h | --help
69                -c <value> | --component=<value>
70                -f <config file> | --file=<config file>
71                -p <project name> | --project=<project name>
72
73SVN Info:
74        $Id: email2trac.py.in 190 2007-09-13 09:45:56Z bas $
75"""
76import os
77import sys
78import string
79import getopt
80import stat
81import time
82import email
[136]83import email.Iterators
84import email.Header
[22]85import re
86import urllib
87import unicodedata
88import ConfigParser
89from stat import *
90import mimetypes
[96]91import traceback
[22]92
[190]93
94# Will fail where unavailable, e.g. Windows
95#
96try:
97    import syslog
98    SYSLOG_AVAILABLE = True
99except ImportError:
100    SYSLOG_AVAILABLE = False
101
[182]102from datetime import tzinfo, timedelta, datetime
[91]103
[182]104
[96]105# Some global variables
106#
[189]107trac_default_version = '0.10'
[96]108m = None
[22]109
[96]110
[182]111
112# A UTC class needed for trac version 0.11, added by
113# tbaschak at ktc dot mb dot ca
114#
115class UTC(tzinfo):
116        """UTC"""
117        ZERO = timedelta(0)
118        HOUR = timedelta(hours=1)
119       
120        def utcoffset(self, dt):
121                return self.ZERO
122               
123        def tzname(self, dt):
124                return "UTC"
125               
126        def dst(self, dt):
127                return self.ZERO
128
129
[22]130class TicketEmailParser(object):
131        env = None
132        comment = '> '
133   
134        def __init__(self, env, parameters, version):
135                self.env = env
136
137                # Database connection
138                #
139                self.db = None
140
[72]141                # Some useful mail constants
142                #
143                self.author = None
144                self.email_addr = None
[183]145                self.email_from = None
[72]146
[22]147                self.VERSION = version
[172]148                self.get_config = self.env.config.get
[22]149
150                if parameters.has_key('umask'):
151                        os.umask(int(parameters['umask'], 8))
152
153                if parameters.has_key('debug'):
154                        self.DEBUG = int(parameters['debug'])
155                else:
156                        self.DEBUG = 0
157
158                if parameters.has_key('mailto_link'):
159                        self.MAILTO = int(parameters['mailto_link'])
[74]160                        if parameters.has_key('mailto_cc'):
161                                self.MAILTO_CC = parameters['mailto_cc']
162                        else:
163                                self.MAILTO_CC = ''
[22]164                else:
165                        self.MAILTO = 0
166
167                if parameters.has_key('spam_level'):
168                        self.SPAM_LEVEL = int(parameters['spam_level'])
169                else:
170                        self.SPAM_LEVEL = 0
171
172                if parameters.has_key('email_comment'):
173                        self.comment = str(parameters['email_comment'])
174
175                if parameters.has_key('email_header'):
176                        self.EMAIL_HEADER = int(parameters['email_header'])
177                else:
178                        self.EMAIL_HEADER = 0
179
[42]180                if parameters.has_key('alternate_notify_template'):
181                        self.notify_template = str(parameters['alternate_notify_template'])
182                else:
183                        self.notify_template = None
[22]184
[43]185                if parameters.has_key('reply_all'):
186                        self.REPLY_ALL = int(parameters['reply_all'])
187                else:
188                        self.REPLY_ALL = 0
[42]189
[74]190                if parameters.has_key('ticket_update'):
191                        self.TICKET_UPDATE = int(parameters['ticket_update'])
192                else:
193                        self.TICKET_UPDATE = 0
[43]194
[118]195                if parameters.has_key('drop_spam'):
196                        self.DROP_SPAM = int(parameters['drop_spam'])
197                else:
198                        self.DROP_SPAM = 0
[74]199
[134]200                if parameters.has_key('verbatim_format'):
201                        self.VERBATIM_FORMAT = int(parameters['verbatim_format'])
202                else:
203                        self.VERBATIM_FORMAT = 1
[118]204
[136]205                if parameters.has_key('strip_signature'):
206                        self.STRIP_SIGNATURE = int(parameters['strip_signature'])
207                else:
208                        self.STRIP_SIGNATURE = 0
[134]209
[148]210                if parameters.has_key('use_textwrap'):
211                        self.USE_TEXTWRAP = int(parameters['use_textwrap'])
212                else:
213                        self.USE_TEXTWRAP = 0
214
[163]215                if parameters.has_key('python_egg_cache'):
216                        self.python_egg_cache = str(parameters['python_egg_cache'])
217                        os.environ['PYTHON_EGG_CACHE'] = self.python_egg_cache
218
[173]219                # Use OS independend functions
220                #
221                self.TMPDIR = os.path.normcase('/tmp')
222                if parameters.has_key('tmpdir'):
223                        self.TMPDIR = os.path.normcase(str(parameters['tmpdir']))
224
[22]225        # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
226        # Note if Spam_level then '*' are included
227        def spam(self, message):
228                if message.has_key('X-Spam-Score'):
229                        spam_l = string.split(message['X-Spam-Score'])
230                        number = spam_l[0].count('*')
231
232                        if number >= self.SPAM_LEVEL:
[41]233                                return 'Spam'
[22]234
[67]235                elif message.has_key('X-Virus-found'):                  # treat virus mails as spam
236                        return 'Spam'
237
[41]238                return self.get_config('ticket', 'default_component')
[22]239
[149]240        def blacklisted_from(self):
241                FROM_RE = re.compile(r"""
242                    MAILER-DAEMON@
243                    """, re.VERBOSE)
244                result =  FROM_RE.search(self.email_addr)
245                if result:
246                        return True
247                else:
248                        return False
249
[139]250        def email_to_unicode(self, message_str):
[22]251                """
252                Email has 7 bit ASCII code, convert it to unicode with the charset
[79]253        that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
[22]254                understands it.
255                """
[139]256                results =  email.Header.decode_header(message_str)
[22]257                str = None
258                for text,format in results:
259                        if format:
260                                try:
261                                        temp = unicode(text, format)
[139]262                                except UnicodeError, detail:
[22]263                                        # This always works
264                                        #
265                                        temp = unicode(text, 'iso-8859-15')
[139]266                                except LookupError, detail:
267                                        #text = 'ERROR: Could not find charset: %s, please install' %format
268                                        #temp = unicode(text, 'iso-8859-15')
269                                        temp = message_str
270                                       
[22]271                        else:
272                                temp = string.strip(text)
[92]273                                temp = unicode(text, 'iso-8859-15')
[22]274
275                        if str:
[100]276                                str = '%s %s' %(str, temp)
[22]277                        else:
[100]278                                str = '%s' %temp
[22]279
[139]280                #str = str.encode('utf-8')
[22]281                return str
282
283        def debug_attachments(self, message):
284                n = 0
285                for part in message.walk():
286                        if part.get_content_maintype() == 'multipart':      # multipart/* is just a container
287                                print 'TD: multipart container'
288                                continue
289
290                        n = n + 1
291                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
292                        print 'TD: part%d: filename: %s' % (n, part.get_filename())
293
294                        if part.is_multipart():
295                                print 'TD: this part is multipart'
296                                payload = part.get_payload(decode=1)
297                                print 'TD: payload:', payload
298                        else:
299                                print 'TD: this part is not multipart'
300
[173]301                        file = 'part%d' %n
302                        part_file = os.path.join(self.TMPDIR, file)
303                        #part_file = '/var/tmp/part%d' % n
[22]304                        print 'TD: writing part%d (%s)' % (n,part_file)
305                        fx = open(part_file, 'wb')
306                        text = part.get_payload(decode=1)
307                        if not text:
308                                text = '(None)'
309                        fx.write(text)
310                        fx.close()
311                        try:
312                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
313                        except OSError:
314                                pass
315
316        def email_header_txt(self, m):
[72]317                """
318                Display To and CC addresses in description field
319                """
[22]320                str = ''
321                if m['To'] and len(m['To']) > 0 and m['To'] != 'hic@sara.nl':
322                        str = "'''To:''' %s [[BR]]" %(m['To'])
323                if m['Cc'] and len(m['Cc']) > 0:
324                        str = "%s'''Cc:''' %s [[BR]]" % (str, m['Cc'])
325
[139]326                return  self.email_to_unicode(str)
[22]327
[138]328
[43]329        def set_owner(self, ticket):
[45]330                """
331                Select default owner for ticket component
332                """
[176]333                #### return self.get_config('ticket', 'default_component')
[43]334                cursor = self.db.cursor()
335                sql = "SELECT owner FROM component WHERE name='%s'" % ticket['component']
336                cursor.execute(sql)
[61]337                try:
338                        ticket['owner'] = cursor.fetchone()[0]
339                except TypeError, detail:
[176]340                        ticket['owner'] = None
[43]341
[72]342        def get_author_emailaddrs(self, message):
[45]343                """
[72]344                Get the default author name and email address from the message
[45]345                """
[143]346                temp = self.email_to_unicode(message['from'])
347                #print temp.encode('utf-8')
[43]348
[143]349                self.author, self.email_addr  = email.Utils.parseaddr(temp)
350                #print self.author.encode('utf-8', 'replace')
[142]351
[45]352                # Look for email address in registered trac users
353                #
[172]354                users = [ u for (u, n, e) in self.env.get_known_users(self.db)
[72]355                                if e == self.email_addr ]
[43]356
[45]357                if len(users) == 1:
[183]358                        self.email_from = users[0]
[45]359                else:
[183]360                        self.email_from =  self.email_to_unicode(message['from'])
361                        #self.email_from =  self.email_to_unicode(self.email_addr)
[45]362
[72]363        def set_reply_fields(self, ticket, message):
364                """
365                Set all the right fields for a new ticket
366                """
[183]367                ticket['reporter'] = self.email_from
[72]368
[45]369                # Put all CC-addresses in ticket CC field
[43]370                #
371                if self.REPLY_ALL:
[45]372                        #tos = message.get_all('to', [])
[43]373                        ccs = message.get_all('cc', [])
374
[45]375                        addrs = email.Utils.getaddresses(ccs)
[105]376                        if not addrs:
377                                return
[43]378
379                        # Remove reporter email address if notification is
380                        # on
381                        #
382                        if self.notification:
383                                try:
[72]384                                        addrs.remove((self.author, self.email_addr))
[43]385                                except ValueError, detail:
386                                        pass
387
[45]388                        for name,mail in addrs:
[105]389                                try:
[108]390                                        mail_list = '%s, %s' %(mail_list, mail)
[105]391                                except:
392                                        mail_list = mail
[43]393
[105]394                        if mail_list:
[139]395                                ticket['cc'] = self.email_to_unicode(mail_list)
[96]396
397        def save_email_for_debug(self, message, tempfile=False):
398                if tempfile:
399                        import tempfile
400                        msg_file = tempfile.mktemp('.email2trac')
401                else:
[173]402                        #msg_file = '/var/tmp/msg.txt'
403                        msg_file = os.path.join(self.TMPDIR, 'msg.txt')
404
[44]405                print 'TD: saving email to %s' % msg_file
406                fx = open(msg_file, 'wb')
407                fx.write('%s' % message)
408                fx.close()
409                try:
410                        os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
411                except OSError:
412                        pass
413
[167]414        def str_to_dict(self, str):
[164]415                """
416                Transfrom a str of the form [<key>=<value>]+ to dict[<key>] = <value>
417                """
418                # Skip the last ':' character
419                #
420                fields = string.split(str[:-1], ',')
421
422                result = dict()
423                for field in fields:
424                        try:
425                                index, value = string.split(field,'=')
[169]426
427                                # We can not change the description of a ticket via the subject
428                                # line. The description is the body of the email
429                                #
430                                if index.lower() in ['description']:
431                                        continue
432
[164]433                                if value:
[165]434                                        result[index.lower()] = value
[169]435
[164]436                        except ValueError:
437                                pass
438
[165]439                return result
[167]440
[169]441        def update_ticket_fields(self, ticket, user_dict):
442                """
443                This will update the ticket fields when supplied via
444                the subject mail line. It will only update the ticket
445                field:
446                        - If the field is known
447                        - If the value supplied is valid for the ticket field
448                        - Else we skip it and no error is given
449                """
450
451                # Build a system dictionary from the ticket fields
452                # with field as index and option as value
453                #
454                sys_dict = dict()
455                for field in ticket.fields:
[167]456                        try:
[169]457                                sys_dict[field['name']] = field['options']
458
[167]459                        except KeyError:
[169]460                                sys_dict[field['name']] = None
[167]461                                pass
[169]462
463                # Check user supplied fields an compare them with the
464                # system one's
465                #
466                for field,value in user_dict.items():
467                        if self.DEBUG >= 5:
468                                print  'user field : %s=%s' %(field,value)
469
470                        if sys_dict.has_key(field):
471                                if self.DEBUG >= 5:
472                                        print  'sys field  : ', sys_dict[field]
473
474                                # Check if value is an allowed system option, if TypeError then
475                                # every value is allowed
476                                #
477                                try:
478                                        if value in sys_dict[field]:
479                                                ticket[field] = value
480
481                                except TypeError:
482                                        ticket[field] = value
483                                       
[164]484                               
[169]485                               
[71]486        def ticket_update(self, m):
[78]487                """
[79]488                If the current email is a reply to an existing ticket, this function
489                will append the contents of this email to that ticket, instead of
490                creating a new one.
[78]491                """
[71]492                if not m['Subject']:
493                        return False
494                else:
[139]495                        subject  = self.email_to_unicode(m['Subject'])
[71]496
[182]497                # [hic] #1529: Re: LRZ
498                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
499                #
[71]500                TICKET_RE = re.compile(r"""
501                                        (?P<ticketnr>[#][0-9]+:)
[164]502                                        |(?P<ticketnr_fields>[#][\d]+\?.*:)
[71]503                                        """, re.VERBOSE)
504
505                result =  TICKET_RE.search(subject)
506                if not result:
507                        return False
508
[164]509                # Must we update ticket fields
510                #
[167]511                update_tkt_fields = dict()
[165]512                try:
[164]513                        nr, keywords = string.split(result.group('ticketnr_fields'), '?')
[167]514                        update_tkt_fields = self.str_to_dict(keywords)
[165]515
516                        # Strip '#'
517                        #
518                        ticket_id = int(nr[1:])
519
520                except AttributeError:
521                        # Strip '#' and ':'
522                        #
[167]523                        nr = result.group('ticketnr')
[165]524                        ticket_id = int(nr[1:-1])
[164]525
[71]526
[77]527                # Get current time
528                #
529                when = int(time.time())
530
[172]531                try:
532                        tkt = Ticket(self.env, ticket_id, self.db)
533                except util.TracError, detail:
534                        return False
[126]535
[172]536                # Must we update some ticket fields properties
537                #
538                if update_tkt_fields:
[169]539                        self.update_ticket_fields(tkt, update_tkt_fields)
[166]540
[172]541                body_text = self.get_body_text(m)
[177]542                if self.EMAIL_HEADER:
543                        head = self.email_header_txt(m)
544                        body_text = u"\r\n%s \r\n%s" %(head, body_text)
[76]545
[177]546                #if self.MAILTO:
547                #       mailto = self.html_mailto_link(tkt['summary'], ticket_id, body_text)
548                #       body_text = u"\r\n%s \r\n%s" %(mailto, body_text)
549
[172]550                tkt.save_changes(self.author, body_text, when)
551                tkt['id'] = ticket_id
552
[129]553                if self.VERSION  == 0.9:
[152]554                        str = self.attachments(m, tkt, True)
[129]555                else:
[152]556                        str = self.attachments(m, tkt)
[76]557
[72]558                if self.notification:
[77]559                        self.notify(tkt, False, when)
[72]560
[71]561                return True
562
[84]563        def new_ticket(self, msg):
[77]564                """
565                Create a new ticket
566                """
[41]567                tkt = Ticket(self.env)
[22]568                tkt['status'] = 'new'
569
570                # Some defaults
571                #
572                tkt['milestone'] = self.get_config('ticket', 'default_milestone')
573                tkt['priority'] = self.get_config('ticket', 'default_priority')
574                tkt['severity'] = self.get_config('ticket', 'default_severity')
575                tkt['version'] = self.get_config('ticket', 'default_version')
576
577                if not msg['Subject']:
[151]578                        tkt['summary'] = u'(No subject)'
[22]579                else:
[139]580                        tkt['summary'] = self.email_to_unicode(msg['Subject'])
[22]581
582
[41]583                if settings.has_key('component'):
[22]584                        tkt['component'] = settings['component']
585                else:
[41]586                        tkt['component'] = self.spam(msg)
[22]587
[118]588                # Discard SPAM messages.
[38]589                #
[122]590                if self.DROP_SPAM and (tkt['component'] == 'Spam'):
[150]591                        if self.DEBUG > 2 :
[149]592                          print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
[124]593                        return False   
[38]594
[176]595                # Set default owner for component, HvB
596                # Is not necessary, because if component is set. The trac code
597                # will find the owner: self.set_owner(tkt)
[22]598                #
[72]599                self.set_reply_fields(tkt, msg)
[22]600
[45]601                # produce e-mail like header
602                #
[22]603                head = ''
604                if self.EMAIL_HEADER > 0:
605                        head = self.email_header_txt(msg)
[92]606                       
[72]607                body_text = self.get_body_text(msg)
[45]608
[140]609                tkt['description'] = '\r\n%s\r\n%s' \
[142]610                        %(head, body_text)
[90]611
[182]612                #when = int(time.time())
613                utc = UTC()
614                when = datetime.now(utc)
[45]615
[172]616                ticket_id = tkt.insert()
[187]617                #try:
618                #       ticket_id = tkt.insert()
619                #except OperationalError, detail:
620                #       syslog.openlog('email2trac', syslog.LOG_NOWAIT)
621                #       syslog.syslog('catch tkt insert problem %s' %detail)
622                #       syslog.closelog()
623                #
624                #       ticket_id = tkt.insert()
625                       
[172]626                tkt['id'] = ticket_id
627
[90]628                changed = False
629                comment = ''
[77]630
[90]631                # Rewrite the description if we have mailto enabled
[45]632                #
[72]633                if self.MAILTO:
[100]634                        changed = True
[142]635                        comment = u'\nadded mailto line\n'
636                        mailto = self.html_mailto_link(tkt['summary'], ticket_id, body_text)
637                        tkt['description'] = u'\r\n%s\r\n%s%s\r\n' \
638                                %(head, mailto, body_text)
[45]639
[152]640                str =  self.attachments(msg, tkt)
641                if str:
[100]642                        changed = True
[152]643                        comment = '%s\n%s\n' %(comment, str)
[77]644
[90]645                if changed:
[172]646                        tkt.save_changes(self.author, comment)
647                        #print tkt.get_changelog(self.db, when)
[90]648
[45]649                if self.notification:
[90]650                        self.notify(tkt, True)
651                        #self.notify(tkt, False)
[45]652
[77]653        def parse(self, fp):
[96]654                global m
655
[77]656                m = email.message_from_file(fp)
657                if not m:
658                        return
659
660                if self.DEBUG > 1:        # save the entire e-mail message text
661                        self.save_email_for_debug(m)
662                        self.debug_attachments(m)
663
664                self.db = self.env.get_db_cnx()
[149]665                self.get_author_emailaddrs(m)
[152]666
[149]667                if self.blacklisted_from():
668                        if self.DEBUG > 1 :
669                                print 'Message rejected : From: in blacklist'
670                        return False
[77]671
672                if self.get_config('notification', 'smtp_enabled') in ['true']:
673                        self.notification = 1
674                else:
675                        self.notification = 0
676
677                # Must we update existing tickets
678                #
679                if self.TICKET_UPDATE > 0:
680                        if self.ticket_update(m):
681                                return True
682
[84]683                self.new_ticket(m)
[77]684
[136]685        def strip_signature(self, text):
686                """
687                Strip signature from message, inspired by Mailman software
688                """
689                body = []
690                for line in text.splitlines():
691                        if line == '-- ':
692                                break
693                        body.append(line)
694
695                return ('\n'.join(body))
696
[151]697
[154]698        def wrap_text(self, text, replace_whitespace = False):
[151]699                """
700                Will break a lines longer then given length into several small lines of size
701                given length
702                """
703                import textwrap
[154]704
[151]705                LINESEPARATOR = '\n'
[153]706                reformat = ''
[151]707
[154]708                for s in text.split(LINESEPARATOR):
709                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
710                        if tmp:
711                                reformat = '%s\n%s' %(reformat,tmp)
712                        else:
713                                reformat = '%s\n' %reformat
[153]714
715                return reformat
716
[154]717                # Python2.4 and higher
718                #
719                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
720                #
721
722
[72]723        def get_body_text(self, msg):
[45]724                """
[79]725                put the message text in the ticket description or in the changes field.
[45]726                message text can be plain text or html or something else
727                """
[22]728                has_description = 0
[100]729                encoding = True
[109]730                ubody_text = u'No plain text message'
[22]731                for part in msg.walk():
[45]732
733                        # 'multipart/*' is a container for multipart messages
734                        #
735                        if part.get_content_maintype() == 'multipart':
[22]736                                continue
737
738                        if part.get_content_type() == 'text/plain':
[45]739                                # Try to decode, if fails then do not decode
740                                #
[90]741                                body_text = part.get_payload(decode=1)
[45]742                                if not body_text:                       
[90]743                                        body_text = part.get_payload(decode=0)
[154]744       
[136]745                                if self.STRIP_SIGNATURE:
746                                        body_text = self.strip_signature(body_text)
[22]747
[148]748                                if self.USE_TEXTWRAP:
[151]749                                        body_text = self.wrap_text(body_text)
[148]750
[45]751                                # Get contents charset (iso-8859-15 if not defined in mail headers)
752                                #
[100]753                                charset = part.get_content_charset()
[102]754                                if not charset:
755                                        charset = 'iso-8859-15'
756
[89]757                                try:
[96]758                                        ubody_text = unicode(body_text, charset)
[100]759
760                                except UnicodeError, detail:
[96]761                                        ubody_text = unicode(body_text, 'iso-8859-15')
[89]762
[100]763                                except LookupError, detail:
[139]764                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
[100]765
[22]766                        elif part.get_content_type() == 'text/html':
[109]767                                ubody_text = '(see attachment for HTML mail message)'
[22]768
769                        else:
[109]770                                ubody_text = '(see attachment for message)'
[22]771
772                        has_description = 1
773                        break           # we have the description, so break
774
775                if not has_description:
[109]776                        ubody_text = '(see attachment for message)'
[22]777
[100]778                # A patch so that the web-interface will not update the description
779                # field of a ticket
780                #
781                ubody_text = ('\r\n'.join(ubody_text.splitlines()))
[22]782
[100]783                #  If we can unicode it try to encode it for trac
784                #  else we a lot of garbage
785                #
[142]786                #if encoding:
787                #       ubody_text = ubody_text.encode('utf-8')
[100]788
[134]789                if self.VERBATIM_FORMAT:
790                        ubody_text = '{{{\r\n%s\r\n}}}' %ubody_text
791                else:
792                        ubody_text = '%s' %ubody_text
793
[100]794                return ubody_text
795
[77]796        def notify(self, tkt , new=True, modtime=0):
[79]797                """
798                A wrapper for the TRAC notify function. So we can use templates
799                """
[112]800                if tkt['component'] == 'Spam':
801                        return 
802
[41]803                try:
804                        # create false {abs_}href properties, to trick Notify()
805                        #
806                        self.env.abs_href = Href(self.get_config('project', 'url'))
807                        self.env.href = Href(self.get_config('project', 'url'))
[22]808
[41]809                        tn = TicketNotifyEmail(self.env)
[42]810                        if self.notify_template:
811                                tn.template_name = self.notify_template;
812
[77]813                        tn.notify(tkt, new, modtime)
[41]814
815                except Exception, e:
[79]816                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(tkt['id'], e)
[41]817
[22]818        def mail_line(self, str):
819                return '%s %s' % (self.comment, str)
820
821
[72]822        def html_mailto_link(self, subject, id, body):
823                if not self.author:
[143]824                        author = self.email_addr
[22]825                else:   
[142]826                        author = self.author
[22]827
828                # Must find a fix
829                #
830                #arr = string.split(body, '\n')
831                #arr = map(self.mail_line, arr)
832                #body = string.join(arr, '\n')
833                #body = '%s wrote:\n%s' %(author, body)
834
835                # Temporary fix
[142]836                #
[74]837                str = 'mailto:%s?Subject=%s&Cc=%s' %(
838                       urllib.quote(self.email_addr),
839                           urllib.quote('Re: #%s: %s' %(id, subject)),
840                           urllib.quote(self.MAILTO_CC)
841                           )
842
[90]843                str = '\r\n{{{\r\n#!html\r\n<a href="%s">Reply to: %s</a>\r\n}}}\r\n' %(str, author)
[22]844                return str
845
[129]846        def attachments(self, message, ticket, update=False):
[79]847                '''
848                save any attachments as files in the ticket's directory
849                '''
[22]850                count = 0
[77]851                first = 0
852                number = 0
[152]853
854                # Get Maxium attachment size
855                #
856                max_size = int(self.get_config('attachment', 'max_size'))
[153]857                status   = ''
[152]858
[22]859                for part in message.walk():
860                        if part.get_content_maintype() == 'multipart':          # multipart/* is just a container
861                                continue
862
863                        if not first:                                                                           # first content is the message
864                                first = 1
865                                if part.get_content_type() == 'text/plain':             # if first is text, is was already put in the description
866                                        continue
867
868                        filename = part.get_filename()
869                        if not filename:
[77]870                                number = number + 1
871                                filename = 'part%04d' % number
[22]872
[72]873                                ext = mimetypes.guess_extension(part.get_content_type())
[22]874                                if not ext:
875                                        ext = '.bin'
876
877                                filename = '%s%s' % (filename, ext)
878                        else:
[139]879                                filename = self.email_to_unicode(filename)
[22]880
[48]881                        # From the trac code
882                        #
883                        filename = filename.replace('\\', '/').replace(':', '/')
884                        filename = os.path.basename(filename)
[22]885
[48]886                        # We try to normalize the filename to utf-8 NFC if we can.
887                        # Files uploaded from OS X might be in NFD.
[92]888                        # Check python version and then try it
[48]889                        #
890                        if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
[92]891                                try:
892                                        filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
893                                except TypeError:
894                                        pass
[48]895
[22]896                        url_filename = urllib.quote(filename)
[172]897                        #
898                        # Must be tuneables HvB
899                        #
[173]900                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, url_filename))
[22]901                        text = part.get_payload(decode=1)
902                        if not text:
903                                text = '(None)'
[48]904                        fd.write(text)
905                        fd.close()
[22]906
[153]907                        # get the file_size
[22]908                        #
[48]909                        stats = os.lstat(path)
[153]910                        file_size = stats[stat.ST_SIZE]
[22]911
[152]912                        # Check if the attachment size is allowed
913                        #
[153]914                        if (max_size != -1) and (file_size > max_size):
915                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
916                                        %(status, filename, file_size, max_size)
[152]917
918                                os.unlink(path)
919                                continue
920                        else:
921                                count = count + 1
922                                       
[172]923                        # Insert the attachment
[73]924                        #
[172]925                        fd = open(path)
926                        att = attachment.Attachment(self.env, 'ticket', ticket['id'])
[73]927
[172]928                        # This will break the ticket_update system, the body_text is vaporized
929                        # ;-(
930                        #
931                        if not update:
932                                att.author = self.author
933                                att.description = self.email_to_unicode('Added by email2trac')
[73]934
[172]935                        att.insert(url_filename, fd, file_size)
936                        #except  util.TracError, detail:
937                        #       print detail
[73]938
[103]939                        # Remove the created temporary filename
940                        #
[172]941                        fd.close()
[103]942                        os.unlink(path)
943
[77]944                # Return how many attachments
945                #
[153]946                status = 'This message has %d attachment(s)\n%s' %(count, status)
947                return status
[22]948
[77]949
[22]950def mkdir_p(dir, mode):
951        '''do a mkdir -p'''
952
953        arr = string.split(dir, '/')
954        path = ''
955        for part in arr:
956                path = '%s/%s' % (path, part)
957                try:
958                        stats = os.stat(path)
959                except OSError:
960                        os.mkdir(path, mode)
961
962
963def ReadConfig(file, name):
964        """
965        Parse the config file
966        """
967
968        if not os.path.isfile(file):
[79]969                print 'File %s does not exist' %file
[22]970                sys.exit(1)
971
972        config = ConfigParser.ConfigParser()
973        try:
974                config.read(file)
975        except ConfigParser.MissingSectionHeaderError,detail:
976                print detail
977                sys.exit(1)
978
979
980        # Use given project name else use defaults
981        #
982        if name:
983                if not config.has_section(name):
[79]984                        print "Not a valid project name: %s" %name
[22]985                        print "Valid names: %s" %config.sections()
986                        sys.exit(1)
987
988                project =  dict()
989                for option in  config.options(name):
990                        project[option] = config.get(name, option)
991
992        else:
993                project = config.defaults()
994
995        return project
996
[87]997
[22]998if __name__ == '__main__':
999        # Default config file
1000        #
[24]1001        configfile = '@email2trac_conf@'
[22]1002        project = ''
1003        component = ''
[87]1004        ENABLE_SYSLOG = 0
1005               
[22]1006        try:
1007                opts, args = getopt.getopt(sys.argv[1:], 'chf:p:', ['component=','help', 'file=', 'project='])
1008        except getopt.error,detail:
1009                print __doc__
1010                print detail
1011                sys.exit(1)
[87]1012       
[22]1013        project_name = None
1014        for opt,value in opts:
1015                if opt in [ '-h', '--help']:
1016                        print __doc__
1017                        sys.exit(0)
1018                elif opt in ['-c', '--component']:
1019                        component = value
1020                elif opt in ['-f', '--file']:
1021                        configfile = value
1022                elif opt in ['-p', '--project']:
1023                        project_name = value
[87]1024       
[22]1025        settings = ReadConfig(configfile, project_name)
1026        if not settings.has_key('project'):
1027                print __doc__
[79]1028                print 'No Trac project is defined in the email2trac config file.'
[22]1029                sys.exit(1)
[87]1030       
[22]1031        if component:
1032                settings['component'] = component
[87]1033       
[22]1034        if settings.has_key('trac_version'):
[189]1035                version = settings['trac_version']
[22]1036        else:
1037                version = trac_default_version
1038
[189]1039
[22]1040        #debug HvB
1041        #print settings
[189]1042
[87]1043        try:
[189]1044                if version == '0.9':
[87]1045                        from trac import attachment
1046                        from trac.env import Environment
1047                        from trac.ticket import Ticket
1048                        from trac.web.href import Href
1049                        from trac import util
1050                        from trac.Notify import TicketNotifyEmail
[189]1051                elif version == '0.10':
[87]1052                        from trac import attachment
1053                        from trac.env import Environment
1054                        from trac.ticket import Ticket
1055                        from trac.web.href import Href
1056                        from trac import util
[139]1057                        #
1058                        # return  util.text.to_unicode(str)
1059                        #
[87]1060                        # see http://projects.edgewall.com/trac/changeset/2799
1061                        from trac.ticket.notification import TicketNotifyEmail
[189]1062                elif version == '0.11':
[182]1063                        from trac import attachment
1064                        from trac.env import Environment
1065                        from trac.ticket import Ticket
1066                        from trac.web.href import Href
1067                        from trac import util
1068                        #
1069                        # return  util.text.to_unicode(str)
1070                        #
1071                        # see http://projects.edgewall.com/trac/changeset/2799
1072                        from trac.ticket.notification import TicketNotifyEmail
[189]1073                else:
1074                        print 'TRAC version %s is not supported' %version
1075                        sys.exit(1)
1076                       
1077                if settings.has_key('enable_syslog'):
[190]1078                        if SYSLOG_AVAILABLE:
1079                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
[182]1080
[87]1081                env = Environment(settings['project'], create=0)
[189]1082                tktparser = TicketEmailParser(env, settings, float(version))
[87]1083                tktparser.parse(sys.stdin)
[22]1084
[87]1085        # Catch all errors ans log to SYSLOG if we have enabled this
1086        # else stdout
1087        #
1088        except Exception, error:
1089                if ENABLE_SYSLOG:
1090                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
[187]1091
[87]1092                        etype, evalue, etb = sys.exc_info()
1093                        for e in traceback.format_exception(etype, evalue, etb):
1094                                syslog.syslog(e)
[187]1095
[87]1096                        syslog.closelog()
1097                else:
1098                        traceback.print_exc()
[22]1099
[97]1100                if m:
[98]1101                        tktparser.save_email_for_debug(m, True)
[97]1102
[22]1103# EOB
Note: See TracBrowser for help on using the repository browser.