source: trunk/email2trac.py.in @ 205

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

email2trac.py.in:

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