source: trunk/email2trac.py.in @ 219

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

email2trac.py.in:

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