source: trunk/email2trac.py.in @ 191

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

email2trac.py.in, ChangeLog?, email2trac.conf:

  • Added strip_quotes function. Author: nicolasm at opera dot com
  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 27.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:
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 191 2007-10-11 10:01:11Z 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
541                #
542                when = int(time.time())
543
[172]544                try:
545                        tkt = Ticket(self.env, ticket_id, self.db)
546                except util.TracError, detail:
547                        return False
[126]548
[172]549                # Must we update some ticket fields properties
550                #
551                if update_tkt_fields:
[169]552                        self.update_ticket_fields(tkt, update_tkt_fields)
[166]553
[172]554                body_text = self.get_body_text(m)
[177]555                if self.EMAIL_HEADER:
556                        head = self.email_header_txt(m)
557                        body_text = u"\r\n%s \r\n%s" %(head, body_text)
[76]558
[177]559                #if self.MAILTO:
560                #       mailto = self.html_mailto_link(tkt['summary'], ticket_id, body_text)
561                #       body_text = u"\r\n%s \r\n%s" %(mailto, body_text)
562
[172]563                tkt.save_changes(self.author, body_text, when)
564                tkt['id'] = ticket_id
565
[129]566                if self.VERSION  == 0.9:
[152]567                        str = self.attachments(m, tkt, True)
[129]568                else:
[152]569                        str = self.attachments(m, tkt)
[76]570
[72]571                if self.notification:
[77]572                        self.notify(tkt, False, when)
[72]573
[71]574                return True
575
[84]576        def new_ticket(self, msg):
[77]577                """
578                Create a new ticket
579                """
[41]580                tkt = Ticket(self.env)
[22]581                tkt['status'] = 'new'
582
583                # Some defaults
584                #
585                tkt['milestone'] = self.get_config('ticket', 'default_milestone')
586                tkt['priority'] = self.get_config('ticket', 'default_priority')
587                tkt['severity'] = self.get_config('ticket', 'default_severity')
588                tkt['version'] = self.get_config('ticket', 'default_version')
589
590                if not msg['Subject']:
[151]591                        tkt['summary'] = u'(No subject)'
[22]592                else:
[139]593                        tkt['summary'] = self.email_to_unicode(msg['Subject'])
[22]594
595
[41]596                if settings.has_key('component'):
[22]597                        tkt['component'] = settings['component']
598                else:
[41]599                        tkt['component'] = self.spam(msg)
[22]600
[118]601                # Discard SPAM messages.
[38]602                #
[122]603                if self.DROP_SPAM and (tkt['component'] == 'Spam'):
[150]604                        if self.DEBUG > 2 :
[149]605                          print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
[124]606                        return False   
[38]607
[176]608                # Set default owner for component, HvB
609                # Is not necessary, because if component is set. The trac code
610                # will find the owner: self.set_owner(tkt)
[22]611                #
[72]612                self.set_reply_fields(tkt, msg)
[22]613
[45]614                # produce e-mail like header
615                #
[22]616                head = ''
617                if self.EMAIL_HEADER > 0:
618                        head = self.email_header_txt(msg)
[92]619                       
[72]620                body_text = self.get_body_text(msg)
[45]621
[140]622                tkt['description'] = '\r\n%s\r\n%s' \
[142]623                        %(head, body_text)
[90]624
[182]625                #when = int(time.time())
626                utc = UTC()
627                when = datetime.now(utc)
[45]628
[172]629                ticket_id = tkt.insert()
[187]630                #try:
631                #       ticket_id = tkt.insert()
632                #except OperationalError, detail:
633                #       syslog.openlog('email2trac', syslog.LOG_NOWAIT)
634                #       syslog.syslog('catch tkt insert problem %s' %detail)
635                #       syslog.closelog()
636                #
637                #       ticket_id = tkt.insert()
638                       
[172]639                tkt['id'] = ticket_id
640
[90]641                changed = False
642                comment = ''
[77]643
[90]644                # Rewrite the description if we have mailto enabled
[45]645                #
[72]646                if self.MAILTO:
[100]647                        changed = True
[142]648                        comment = u'\nadded mailto line\n'
649                        mailto = self.html_mailto_link(tkt['summary'], ticket_id, body_text)
650                        tkt['description'] = u'\r\n%s\r\n%s%s\r\n' \
651                                %(head, mailto, body_text)
[45]652
[152]653                str =  self.attachments(msg, tkt)
654                if str:
[100]655                        changed = True
[152]656                        comment = '%s\n%s\n' %(comment, str)
[77]657
[90]658                if changed:
[172]659                        tkt.save_changes(self.author, comment)
660                        #print tkt.get_changelog(self.db, when)
[90]661
[45]662                if self.notification:
[90]663                        self.notify(tkt, True)
664                        #self.notify(tkt, False)
[45]665
[77]666        def parse(self, fp):
[96]667                global m
668
[77]669                m = email.message_from_file(fp)
670                if not m:
671                        return
672
673                if self.DEBUG > 1:        # save the entire e-mail message text
674                        self.save_email_for_debug(m)
675                        self.debug_attachments(m)
676
677                self.db = self.env.get_db_cnx()
[149]678                self.get_author_emailaddrs(m)
[152]679
[149]680                if self.blacklisted_from():
681                        if self.DEBUG > 1 :
682                                print 'Message rejected : From: in blacklist'
683                        return False
[77]684
685                if self.get_config('notification', 'smtp_enabled') in ['true']:
686                        self.notification = 1
687                else:
688                        self.notification = 0
689
690                # Must we update existing tickets
691                #
692                if self.TICKET_UPDATE > 0:
693                        if self.ticket_update(m):
694                                return True
695
[84]696                self.new_ticket(m)
[77]697
[136]698        def strip_signature(self, text):
699                """
700                Strip signature from message, inspired by Mailman software
701                """
702                body = []
703                for line in text.splitlines():
704                        if line == '-- ':
705                                break
706                        body.append(line)
707
708                return ('\n'.join(body))
709
[191]710        def strip_quotes(self, text):
711        """
712        Strip quotes from message by Nicolas Mendoza
713        """
714        body = []
715        for line in text.splitlines():
716                if line.startswith(self.EMAIL_QUOTE):
717                        continue
718                body.append(line)
[151]719
[191]720        return ('\n'.join(body))
721
722
[154]723        def wrap_text(self, text, replace_whitespace = False):
[151]724                """
[191]725                Will break a lines longer then given length into several small
726                lines of size given length
[151]727                """
728                import textwrap
[154]729
[151]730                LINESEPARATOR = '\n'
[153]731                reformat = ''
[151]732
[154]733                for s in text.split(LINESEPARATOR):
734                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
735                        if tmp:
736                                reformat = '%s\n%s' %(reformat,tmp)
737                        else:
738                                reformat = '%s\n' %reformat
[153]739
740                return reformat
741
[154]742                # Python2.4 and higher
743                #
744                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
745                #
746
747
[72]748        def get_body_text(self, msg):
[45]749                """
[79]750                put the message text in the ticket description or in the changes field.
[45]751                message text can be plain text or html or something else
752                """
[22]753                has_description = 0
[100]754                encoding = True
[109]755                ubody_text = u'No plain text message'
[22]756                for part in msg.walk():
[45]757
758                        # 'multipart/*' is a container for multipart messages
759                        #
760                        if part.get_content_maintype() == 'multipart':
[22]761                                continue
762
763                        if part.get_content_type() == 'text/plain':
[45]764                                # Try to decode, if fails then do not decode
765                                #
[90]766                                body_text = part.get_payload(decode=1)
[45]767                                if not body_text:                       
[90]768                                        body_text = part.get_payload(decode=0)
[154]769       
[136]770                                if self.STRIP_SIGNATURE:
771                                        body_text = self.strip_signature(body_text)
[22]772
[191]773                                if self.STRIP_QUOTES:
774                                        body_text = self.strip_quotes(body_text)
775
[148]776                                if self.USE_TEXTWRAP:
[151]777                                        body_text = self.wrap_text(body_text)
[148]778
[45]779                                # Get contents charset (iso-8859-15 if not defined in mail headers)
780                                #
[100]781                                charset = part.get_content_charset()
[102]782                                if not charset:
783                                        charset = 'iso-8859-15'
784
[89]785                                try:
[96]786                                        ubody_text = unicode(body_text, charset)
[100]787
788                                except UnicodeError, detail:
[96]789                                        ubody_text = unicode(body_text, 'iso-8859-15')
[89]790
[100]791                                except LookupError, detail:
[139]792                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
[100]793
[22]794                        elif part.get_content_type() == 'text/html':
[109]795                                ubody_text = '(see attachment for HTML mail message)'
[22]796
797                        else:
[109]798                                ubody_text = '(see attachment for message)'
[22]799
800                        has_description = 1
801                        break           # we have the description, so break
802
803                if not has_description:
[109]804                        ubody_text = '(see attachment for message)'
[22]805
[100]806                # A patch so that the web-interface will not update the description
807                # field of a ticket
808                #
809                ubody_text = ('\r\n'.join(ubody_text.splitlines()))
[22]810
[100]811                #  If we can unicode it try to encode it for trac
812                #  else we a lot of garbage
813                #
[142]814                #if encoding:
815                #       ubody_text = ubody_text.encode('utf-8')
[100]816
[134]817                if self.VERBATIM_FORMAT:
818                        ubody_text = '{{{\r\n%s\r\n}}}' %ubody_text
819                else:
820                        ubody_text = '%s' %ubody_text
821
[100]822                return ubody_text
823
[77]824        def notify(self, tkt , new=True, modtime=0):
[79]825                """
826                A wrapper for the TRAC notify function. So we can use templates
827                """
[112]828                if tkt['component'] == 'Spam':
829                        return 
830
[41]831                try:
832                        # create false {abs_}href properties, to trick Notify()
833                        #
834                        self.env.abs_href = Href(self.get_config('project', 'url'))
835                        self.env.href = Href(self.get_config('project', 'url'))
[22]836
[41]837                        tn = TicketNotifyEmail(self.env)
[42]838                        if self.notify_template:
839                                tn.template_name = self.notify_template;
840
[77]841                        tn.notify(tkt, new, modtime)
[41]842
843                except Exception, e:
[79]844                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(tkt['id'], e)
[41]845
[72]846        def html_mailto_link(self, subject, id, body):
847                if not self.author:
[143]848                        author = self.email_addr
[22]849                else:   
[142]850                        author = self.author
[22]851
852                # Must find a fix
853                #
854                #arr = string.split(body, '\n')
855                #arr = map(self.mail_line, arr)
856                #body = string.join(arr, '\n')
857                #body = '%s wrote:\n%s' %(author, body)
858
859                # Temporary fix
[142]860                #
[74]861                str = 'mailto:%s?Subject=%s&Cc=%s' %(
862                       urllib.quote(self.email_addr),
863                           urllib.quote('Re: #%s: %s' %(id, subject)),
864                           urllib.quote(self.MAILTO_CC)
865                           )
866
[90]867                str = '\r\n{{{\r\n#!html\r\n<a href="%s">Reply to: %s</a>\r\n}}}\r\n' %(str, author)
[22]868                return str
869
[129]870        def attachments(self, message, ticket, update=False):
[79]871                '''
872                save any attachments as files in the ticket's directory
873                '''
[22]874                count = 0
[77]875                first = 0
876                number = 0
[152]877
878                # Get Maxium attachment size
879                #
880                max_size = int(self.get_config('attachment', 'max_size'))
[153]881                status   = ''
[152]882
[22]883                for part in message.walk():
884                        if part.get_content_maintype() == 'multipart':          # multipart/* is just a container
885                                continue
886
887                        if not first:                                                                           # first content is the message
888                                first = 1
889                                if part.get_content_type() == 'text/plain':             # if first is text, is was already put in the description
890                                        continue
891
892                        filename = part.get_filename()
893                        if not filename:
[77]894                                number = number + 1
895                                filename = 'part%04d' % number
[22]896
[72]897                                ext = mimetypes.guess_extension(part.get_content_type())
[22]898                                if not ext:
899                                        ext = '.bin'
900
901                                filename = '%s%s' % (filename, ext)
902                        else:
[139]903                                filename = self.email_to_unicode(filename)
[22]904
[48]905                        # From the trac code
906                        #
907                        filename = filename.replace('\\', '/').replace(':', '/')
908                        filename = os.path.basename(filename)
[22]909
[48]910                        # We try to normalize the filename to utf-8 NFC if we can.
911                        # Files uploaded from OS X might be in NFD.
[92]912                        # Check python version and then try it
[48]913                        #
914                        if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
[92]915                                try:
916                                        filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
917                                except TypeError:
918                                        pass
[48]919
[22]920                        url_filename = urllib.quote(filename)
[172]921                        #
922                        # Must be tuneables HvB
923                        #
[173]924                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, url_filename))
[22]925                        text = part.get_payload(decode=1)
926                        if not text:
927                                text = '(None)'
[48]928                        fd.write(text)
929                        fd.close()
[22]930
[153]931                        # get the file_size
[22]932                        #
[48]933                        stats = os.lstat(path)
[153]934                        file_size = stats[stat.ST_SIZE]
[22]935
[152]936                        # Check if the attachment size is allowed
937                        #
[153]938                        if (max_size != -1) and (file_size > max_size):
939                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
940                                        %(status, filename, file_size, max_size)
[152]941
942                                os.unlink(path)
943                                continue
944                        else:
945                                count = count + 1
946                                       
[172]947                        # Insert the attachment
[73]948                        #
[172]949                        fd = open(path)
950                        att = attachment.Attachment(self.env, 'ticket', ticket['id'])
[73]951
[172]952                        # This will break the ticket_update system, the body_text is vaporized
953                        # ;-(
954                        #
955                        if not update:
956                                att.author = self.author
957                                att.description = self.email_to_unicode('Added by email2trac')
[73]958
[172]959                        att.insert(url_filename, fd, file_size)
960                        #except  util.TracError, detail:
961                        #       print detail
[73]962
[103]963                        # Remove the created temporary filename
964                        #
[172]965                        fd.close()
[103]966                        os.unlink(path)
967
[77]968                # Return how many attachments
969                #
[153]970                status = 'This message has %d attachment(s)\n%s' %(count, status)
971                return status
[22]972
[77]973
[22]974def mkdir_p(dir, mode):
975        '''do a mkdir -p'''
976
977        arr = string.split(dir, '/')
978        path = ''
979        for part in arr:
980                path = '%s/%s' % (path, part)
981                try:
982                        stats = os.stat(path)
983                except OSError:
984                        os.mkdir(path, mode)
985
986
987def ReadConfig(file, name):
988        """
989        Parse the config file
990        """
991
992        if not os.path.isfile(file):
[79]993                print 'File %s does not exist' %file
[22]994                sys.exit(1)
995
996        config = ConfigParser.ConfigParser()
997        try:
998                config.read(file)
999        except ConfigParser.MissingSectionHeaderError,detail:
1000                print detail
1001                sys.exit(1)
1002
1003
1004        # Use given project name else use defaults
1005        #
1006        if name:
1007                if not config.has_section(name):
[79]1008                        print "Not a valid project name: %s" %name
[22]1009                        print "Valid names: %s" %config.sections()
1010                        sys.exit(1)
1011
1012                project =  dict()
1013                for option in  config.options(name):
1014                        project[option] = config.get(name, option)
1015
1016        else:
1017                project = config.defaults()
1018
1019        return project
1020
[87]1021
[22]1022if __name__ == '__main__':
1023        # Default config file
1024        #
[24]1025        configfile = '@email2trac_conf@'
[22]1026        project = ''
1027        component = ''
[87]1028        ENABLE_SYSLOG = 0
1029               
[22]1030        try:
1031                opts, args = getopt.getopt(sys.argv[1:], 'chf:p:', ['component=','help', 'file=', 'project='])
1032        except getopt.error,detail:
1033                print __doc__
1034                print detail
1035                sys.exit(1)
[87]1036       
[22]1037        project_name = None
1038        for opt,value in opts:
1039                if opt in [ '-h', '--help']:
1040                        print __doc__
1041                        sys.exit(0)
1042                elif opt in ['-c', '--component']:
1043                        component = value
1044                elif opt in ['-f', '--file']:
1045                        configfile = value
1046                elif opt in ['-p', '--project']:
1047                        project_name = value
[87]1048       
[22]1049        settings = ReadConfig(configfile, project_name)
1050        if not settings.has_key('project'):
1051                print __doc__
[79]1052                print 'No Trac project is defined in the email2trac config file.'
[22]1053                sys.exit(1)
[87]1054       
[22]1055        if component:
1056                settings['component'] = component
[87]1057       
[22]1058        if settings.has_key('trac_version'):
[189]1059                version = settings['trac_version']
[22]1060        else:
1061                version = trac_default_version
1062
[189]1063
[22]1064        #debug HvB
1065        #print settings
[189]1066
[87]1067        try:
[189]1068                if version == '0.9':
[87]1069                        from trac import attachment
1070                        from trac.env import Environment
1071                        from trac.ticket import Ticket
1072                        from trac.web.href import Href
1073                        from trac import util
1074                        from trac.Notify import TicketNotifyEmail
[189]1075                elif version == '0.10':
[87]1076                        from trac import attachment
1077                        from trac.env import Environment
1078                        from trac.ticket import Ticket
1079                        from trac.web.href import Href
1080                        from trac import util
[139]1081                        #
1082                        # return  util.text.to_unicode(str)
1083                        #
[87]1084                        # see http://projects.edgewall.com/trac/changeset/2799
1085                        from trac.ticket.notification import TicketNotifyEmail
[189]1086                elif version == '0.11':
[182]1087                        from trac import attachment
1088                        from trac.env import Environment
1089                        from trac.ticket import Ticket
1090                        from trac.web.href import Href
1091                        from trac import util
1092                        #
1093                        # return  util.text.to_unicode(str)
1094                        #
1095                        # see http://projects.edgewall.com/trac/changeset/2799
1096                        from trac.ticket.notification import TicketNotifyEmail
[189]1097                else:
1098                        print 'TRAC version %s is not supported' %version
1099                        sys.exit(1)
1100                       
1101                if settings.has_key('enable_syslog'):
[190]1102                        if SYSLOG_AVAILABLE:
1103                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
[182]1104
[87]1105                env = Environment(settings['project'], create=0)
[189]1106                tktparser = TicketEmailParser(env, settings, float(version))
[87]1107                tktparser.parse(sys.stdin)
[22]1108
[87]1109        # Catch all errors ans log to SYSLOG if we have enabled this
1110        # else stdout
1111        #
1112        except Exception, error:
1113                if ENABLE_SYSLOG:
1114                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
[187]1115
[87]1116                        etype, evalue, etb = sys.exc_info()
1117                        for e in traceback.format_exception(etype, evalue, etb):
1118                                syslog.syslog(e)
[187]1119
[87]1120                        syslog.closelog()
1121                else:
1122                        traceback.print_exc()
[22]1123
[97]1124                if m:
[98]1125                        tktparser.save_email_for_debug(m, True)
[97]1126
[22]1127# EOB
Note: See TracBrowser for help on using the repository browser.