source: trunk/email2trac.py.in @ 193

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

email2trac.py.in:

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