source: trunk/email2trac.py.in @ 202

Last change on this file since 202 was 202, checked in by bas, 16 years ago

email2trac.py.in:

  • added set_ticket_fields function, #55
  • added option -t,--ticket_prefix, #55
  • removed obsolete code

run_email2trac.c:

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