source: trunk/email2trac.py.in @ 204

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

email2trac.py.in:

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