source: trunk/email2trac.py.in @ 202

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

email2trac.py.in:

  • added set_ticket_fields function, #55
  • added option -t,--ticket_prefix, #55
  • removed obsolete code

run_email2trac.c:

  • added some patches from redhat
  • 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                -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 202 2008-05-27 20:43:52Z bas $
75"""
76import os
77import sys
78import string
79import getopt
80import stat
81import time
82import email
83import email.Iterators
84import email.Header
85import re
86import urllib
87import unicodedata
88import ConfigParser
89from stat import *
90import mimetypes
91import traceback
92
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
102from datetime import tzinfo, timedelta, datetime
103from trac import config as trac_config
104
105# Some global variables
106#
107trac_default_version = '0.10'
108m = None
109DRY_RUN = False
110
111
112
113# A UTC class needed for trac version 0.11, added by
114# tbaschak at ktc dot mb dot ca
115#
116class UTC(tzinfo):
117        """UTC"""
118        ZERO = timedelta(0)
119        HOUR = timedelta(hours=1)
120       
121        def utcoffset(self, dt):
122                return self.ZERO
123               
124        def tzname(self, dt):
125                return "UTC"
126               
127        def dst(self, dt):
128                return self.ZERO
129
130
131class TicketEmailParser(object):
132        env = None
133        comment = '> '
134   
135        def __init__(self, env, parameters, version):
136                self.env = env
137
138                # Database connection
139                #
140                self.db = None
141
142                # Some useful mail constants
143                #
144                self.author = None
145                self.email_addr = None
146                self.email_from = None
147
148                self.VERSION = version
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 True     
263
264                elif spam:
265
266                        return 'Spam'
267
268                else:
269
270                        return self.get_config('ticket', 'default_component')
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):
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                global DRY_RUN
539
540                if not m['Subject']:
541                        return False
542                else:
543                        subject  = self.email_to_unicode(m['Subject'])
544
545                # [hic] #1529: Re: LRZ
546                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
547                #
548                TICKET_RE = re.compile(r"""
549                                        (?P<ticketnr>[#][0-9]+:)
550                                        |(?P<ticketnr_fields>[#][\d]+\?.*?:)
551                                        """, re.VERBOSE)
552
553                result =  TICKET_RE.search(subject)
554                if not result:
555                        return False
556
557                # Must we update ticket fields
558                #
559                update_tkt_fields = dict()
560                try:
561                        nr, keywords = string.split(result.group('ticketnr_fields'), '?')
562                        update_tkt_fields = self.str_to_dict(keywords)
563
564                        # Strip '#'
565                        #
566                        ticket_id = int(nr[1:])
567
568                except AttributeError:
569                        # Strip '#' and ':'
570                        #
571                        nr = result.group('ticketnr')
572                        ticket_id = int(nr[1:-1])
573
574
575                # When is the change committed
576                #
577                #
578                if self.VERSION == 0.11:
579                        utc = UTC()
580                        when = datetime.now(utc)
581                else:
582                        when = int(time.time())
583
584                try:
585                        tkt = Ticket(self.env, ticket_id, self.db)
586                except util.TracError, detail:
587                        return False
588
589                # Must we update some ticket fields properties
590                #
591                if update_tkt_fields:
592                        self.update_ticket_fields(tkt, update_tkt_fields)
593
594                body_text = self.get_body_text(m)
595                if self.EMAIL_HEADER:
596                        head = self.email_header_txt(m)
597                        body_text = u"\r\n%s \r\n%s" %(head, body_text)
598
599                #if self.MAILTO:
600                #       mailto = self.html_mailto_link(tkt['summary'], ticket_id, body_text)
601                #       body_text = u"\r\n%s \r\n%s" %(mailto, body_text)
602
603                tkt.save_changes(self.author, body_text, when)
604                tkt['id'] = ticket_id
605
606                if self.VERSION  == 0.9:
607                        str = self.attachments(m, tkt, True)
608                else:
609                        str = self.attachments(m, tkt)
610
611                if self.notification:
612                        self.notify(tkt, False, when)
613
614                return True
615
616        def set_ticket_fields(self, ticket):
617                """
618                set the ticket fields to value specified
619                        - /etc/email2trac.conf with <prefix>_<field>
620                        - trac default values, trac.ini
621                """
622                user_dict = dict()
623
624                for field in ticket.fields:
625
626                        name = field['name']
627
628                        # default trac value
629                        #
630                        value = self.get_config('ticket', 'default_%s' %(name) )
631                        if self.DEBUG > 10:
632                                print 'trac.ini name %s = %s' %(name, value)
633
634                        prefix = settings['ticket_prefix']
635                        try:
636                                value = settings['%s_%s' %(prefix, name)]
637                                if self.DEBUG > 10:
638                                        print 'email2trac.conf %s = %s ' %(name, value)
639
640                        except KeyError, detail:
641                                pass
642               
643                        if self.DEBUG:
644                                print 'user_dict[%s] = %s' %(name, value)
645
646                        user_dict[name] = value
647
648                self.update_ticket_fields(ticket, user_dict, use_default=1)
649
650                # Set status ticket
651                #`
652                ticket['status'] = 'new'
653
654
655
656        def new_ticket(self, msg):
657                """
658                Create a new ticket
659                """
660                tkt = Ticket(self.env)
661
662                self.set_ticket_fields(tkt)
663
664                # Some defaults
665                #
666                #tkt['status'] = 'new'
667                #tkt['milestone'] = self.get_config('ticket', 'default_milestone')
668                #tkt['priority'] = self.get_config('ticket', 'default_priority')
669                #tkt['severity'] = self.get_config('ticket', 'default_severity')
670                #tkt['version'] = self.get_config('ticket', 'default_version')
671                #tkt['type'] = self.get_config('ticket', 'default_type')
672
673                # Old style setting for component, will be removed
674                #
675                if settings.has_key('component'):
676                        tkt['component'] = settings['component']
677
678                if not msg['Subject']:
679                        tkt['summary'] = u'(No subject)'
680                else:
681                        tkt['summary'] = self.email_to_unicode(msg['Subject'])
682
683
684                self.set_reply_fields(tkt, msg)
685
686                # produce e-mail like header
687                #
688                head = ''
689                if self.EMAIL_HEADER > 0:
690                        head = self.email_header_txt(msg)
691                       
692                body_text = self.get_body_text(msg)
693
694                tkt['description'] = '\r\n%s\r\n%s' \
695                        %(head, body_text)
696
697                #when = int(time.time())
698                #
699                utc = UTC()
700                when = datetime.now(utc)
701
702                if DRY_RUN:
703                        ticket_id = 'DRY_RUN'
704                else:
705                        ticket_id = tkt.insert()
706                       
707                tkt['id'] = ticket_id
708
709                changed = False
710                comment = ''
711
712                # Rewrite the description if we have mailto enabled
713                #
714                if self.MAILTO:
715                        changed = True
716                        comment = u'\nadded mailto line\n'
717                        mailto = self.html_mailto_link(tkt['summary'], ticket_id, body_text)
718                        tkt['description'] = u'\r\n%s\r\n%s%s\r\n' \
719                                %(head, mailto, body_text)
720
721                str =  self.attachments(msg, tkt)
722                if str:
723                        changed = True
724                        comment = '%s\n%s\n' %(comment, str)
725
726                if changed:
727                        if DRY_RUN:
728                                print 'DRY_RUN: tkt.save_changes(self.author, comment)'
729                        else:
730                                tkt.save_changes(self.author, comment)
731                                #print tkt.get_changelog(self.db, when)
732
733                if self.notification:
734                        if DRY_RUN:
735                                print 'DRY_RUN: self.notify(tkt, True)'
736                        else:
737                                self.notify(tkt, True)
738                                #self.notify(tkt, False)
739
740        def parse(self, fp):
741                global m
742
743                m = email.message_from_file(fp)
744                if not m:
745                        return
746
747                if self.DEBUG > 1:        # save the entire e-mail message text
748                        self.save_email_for_debug(m)
749                        self.debug_attachments(m)
750
751                self.db = self.env.get_db_cnx()
752                self.get_sender_info(m)
753
754                if self.blacklisted_from():
755                        if self.DEBUG > 1 :
756                                print 'Message rejected : From: in blacklist'
757                        return False
758
759            # If component is true then we drop the message drop message
760                #
761                if self.spam(m) == True:
762                        return False
763
764
765                if self.get_config('notification', 'smtp_enabled') in ['true']:
766                        self.notification = 1
767                else:
768                        self.notification = 0
769
770                # Must we update existing tickets
771                #
772                if self.TICKET_UPDATE > 0:
773                        if self.ticket_update(m):
774                                return True
775
776                self.new_ticket(m)
777
778        def strip_signature(self, text):
779                """
780                Strip signature from message, inspired by Mailman software
781                """
782                body = []
783                for line in text.splitlines():
784                        if line == '-- ':
785                                break
786                        body.append(line)
787
788                return ('\n'.join(body))
789
790        def strip_quotes(self, text):
791                """
792                Strip quotes from message by Nicolas Mendoza
793                """
794                body = []
795                for line in text.splitlines():
796                        if line.startswith(self.EMAIL_QUOTE):
797                                continue
798                        body.append(line)
799
800                return ('\n'.join(body))
801
802        def wrap_text(self, text, replace_whitespace = False):
803                """
804                Will break a lines longer then given length into several small
805                lines of size given length
806                """
807                import textwrap
808
809                LINESEPARATOR = '\n'
810                reformat = ''
811
812                for s in text.split(LINESEPARATOR):
813                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
814                        if tmp:
815                                reformat = '%s\n%s' %(reformat,tmp)
816                        else:
817                                reformat = '%s\n' %reformat
818
819                return reformat
820
821                # Python2.4 and higher
822                #
823                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
824                #
825
826
827        def get_body_text(self, msg):
828                """
829                put the message text in the ticket description or in the changes field.
830                message text can be plain text or html or something else
831                """
832                has_description = 0
833                encoding = True
834                ubody_text = u'No plain text message'
835                for part in msg.walk():
836
837                        # 'multipart/*' is a container for multipart messages
838                        #
839                        if part.get_content_maintype() == 'multipart':
840                                continue
841
842                        if part.get_content_type() == 'text/plain':
843                                # Try to decode, if fails then do not decode
844                                #
845                                body_text = part.get_payload(decode=1)
846                                if not body_text:                       
847                                        body_text = part.get_payload(decode=0)
848       
849                                if self.STRIP_SIGNATURE:
850                                        body_text = self.strip_signature(body_text)
851
852                                if self.STRIP_QUOTES:
853                                        body_text = self.strip_quotes(body_text)
854
855                                if self.USE_TEXTWRAP:
856                                        body_text = self.wrap_text(body_text)
857
858                                # Get contents charset (iso-8859-15 if not defined in mail headers)
859                                #
860                                charset = part.get_content_charset()
861                                if not charset:
862                                        charset = 'iso-8859-15'
863
864                                try:
865                                        ubody_text = unicode(body_text, charset)
866
867                                except UnicodeError, detail:
868                                        ubody_text = unicode(body_text, 'iso-8859-15')
869
870                                except LookupError, detail:
871                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
872
873                        elif part.get_content_type() == 'text/html':
874                                ubody_text = '(see attachment for HTML mail message)'
875
876                        else:
877                                ubody_text = '(see attachment for message)'
878
879                        has_description = 1
880                        break           # we have the description, so break
881
882                if not has_description:
883                        ubody_text = '(see attachment for message)'
884
885                # A patch so that the web-interface will not update the description
886                # field of a ticket
887                #
888                ubody_text = ('\r\n'.join(ubody_text.splitlines()))
889
890                #  If we can unicode it try to encode it for trac
891                #  else we a lot of garbage
892                #
893                #if encoding:
894                #       ubody_text = ubody_text.encode('utf-8')
895
896                if self.VERBATIM_FORMAT:
897                        ubody_text = '{{{\r\n%s\r\n}}}' %ubody_text
898                else:
899                        ubody_text = '%s' %ubody_text
900
901                return ubody_text
902
903        def notify(self, tkt , new=True, modtime=0):
904                """
905                A wrapper for the TRAC notify function. So we can use templates
906                """
907                if tkt['component'] == 'Spam':
908                        return 
909
910                try:
911                        # create false {abs_}href properties, to trick Notify()
912                        #
913                        if not self.VERSION == 0.11:
914                                self.env.abs_href = Href(self.get_config('project', 'url'))
915                                self.env.href = Href(self.get_config('project', 'url'))
916
917                        tn = TicketNotifyEmail(self.env)
918                        if self.notify_template:
919                                tn.template_name = self.notify_template;
920
921                        tn.notify(tkt, new, modtime)
922
923                except Exception, e:
924                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(tkt['id'], e)
925
926        def html_mailto_link(self, subject, id, body):
927                if not self.author:
928                        author = self.email_addr
929                else:   
930                        author = self.author
931
932                # Must find a fix
933                #
934                #arr = string.split(body, '\n')
935                #arr = map(self.mail_line, arr)
936                #body = string.join(arr, '\n')
937                #body = '%s wrote:\n%s' %(author, body)
938
939                # Temporary fix
940                #
941                str = 'mailto:%s?Subject=%s&Cc=%s' %(
942                       urllib.quote(self.email_addr),
943                           urllib.quote('Re: #%s: %s' %(id, subject)),
944                           urllib.quote(self.MAILTO_CC)
945                           )
946
947                str = '\r\n{{{\r\n#!html\r\n<a href="%s">Reply to: %s</a>\r\n}}}\r\n' %(str, author)
948                return str
949
950        def attachments(self, message, ticket, update=False):
951                '''
952                save any attachments as files in the ticket's directory
953                '''
954                count = 0
955                first = 0
956                number = 0
957
958                # Get Maxium attachment size
959                #
960                max_size = int(self.get_config('attachment', 'max_size'))
961                status   = ''
962
963                for part in message.walk():
964                        if part.get_content_maintype() == 'multipart':          # multipart/* is just a container
965                                continue
966
967                        if not first:                                                                           # first content is the message
968                                first = 1
969                                if part.get_content_type() == 'text/plain':             # if first is text, is was already put in the description
970                                        continue
971
972                        filename = part.get_filename()
973                        if not filename:
974                                number = number + 1
975                                filename = 'part%04d' % number
976
977                                ext = mimetypes.guess_extension(part.get_content_type())
978                                if not ext:
979                                        ext = '.bin'
980
981                                filename = '%s%s' % (filename, ext)
982                        else:
983                                filename = self.email_to_unicode(filename)
984
985                        # From the trac code
986                        #
987                        filename = filename.replace('\\', '/').replace(':', '/')
988                        filename = os.path.basename(filename)
989
990                        # We try to normalize the filename to utf-8 NFC if we can.
991                        # Files uploaded from OS X might be in NFD.
992                        # Check python version and then try it
993                        #
994                        if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
995                                try:
996                                        filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
997                                except TypeError:
998                                        pass
999
1000                        url_filename = urllib.quote(filename)
1001                        #
1002                        # Must be tuneables HvB
1003                        #
1004                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, url_filename))
1005                        text = part.get_payload(decode=1)
1006                        if not text:
1007                                text = '(None)'
1008                        fd.write(text)
1009                        fd.close()
1010
1011                        # get the file_size
1012                        #
1013                        stats = os.lstat(path)
1014                        file_size = stats[stat.ST_SIZE]
1015
1016                        # Check if the attachment size is allowed
1017                        #
1018                        if (max_size != -1) and (file_size > max_size):
1019                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1020                                        %(status, filename, file_size, max_size)
1021
1022                                os.unlink(path)
1023                                continue
1024                        else:
1025                                count = count + 1
1026                                       
1027                        # Insert the attachment
1028                        #
1029                        fd = open(path)
1030                        att = attachment.Attachment(self.env, 'ticket', ticket['id'])
1031
1032                        # This will break the ticket_update system, the body_text is vaporized
1033                        # ;-(
1034                        #
1035                        if not update:
1036                                att.author = self.author
1037                                att.description = self.email_to_unicode('Added by email2trac')
1038
1039                        att.insert(url_filename, fd, file_size)
1040                        #except  util.TracError, detail:
1041                        #       print detail
1042
1043                        # Remove the created temporary filename
1044                        #
1045                        fd.close()
1046                        os.unlink(path)
1047
1048                # Return how many attachments
1049                #
1050                status = 'This message has %d attachment(s)\n%s' %(count, status)
1051                return status
1052
1053
1054def mkdir_p(dir, mode):
1055        '''do a mkdir -p'''
1056
1057        arr = string.split(dir, '/')
1058        path = ''
1059        for part in arr:
1060                path = '%s/%s' % (path, part)
1061                try:
1062                        stats = os.stat(path)
1063                except OSError:
1064                        os.mkdir(path, mode)
1065
1066def ReadConfig(file, name):
1067        """
1068        Parse the config file
1069        """
1070
1071        if not os.path.isfile(file):
1072                print 'File %s does not exist' %file
1073                sys.exit(1)
1074
1075        config = trac_config.Configuration(file)
1076
1077        # Use given project name else use defaults
1078        #
1079        if name:
1080                sections = config.sections()
1081                if not name in sections:
1082                        print "Not a valid project name: %s" %name
1083                        print "Valid names: %s" %sections
1084                        sys.exit(1)
1085
1086                project =  dict()
1087                for option, value in  config.options(name):
1088                        project[option] = value
1089
1090        else:
1091                project = config.defaults()
1092
1093        return project
1094
1095
1096if __name__ == '__main__':
1097        # Default config file
1098        #
1099        global DRY_RUN
1100
1101        configfile = '@email2trac_conf@'
1102        project = ''
1103        component = ''
1104        ticket_prefix = 'default'
1105
1106        ENABLE_SYSLOG = 0
1107
1108        SHORT_OPT = 'chf:np:t:'
1109        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=']
1110
1111        try:
1112                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
1113        except getopt.error,detail:
1114                print __doc__
1115                print detail
1116                sys.exit(1)
1117       
1118        project_name = None
1119        for opt,value in opts:
1120                if opt in [ '-h', '--help']:
1121                        print __doc__
1122                        sys.exit(0)
1123                elif opt in ['-c', '--component']:
1124                        component = value
1125                elif opt in ['-f', '--file']:
1126                        configfile = value
1127                elif opt in ['-n', '--dry-run']:
1128                        DRY_RUN = True
1129                elif opt in ['-p', '--project']:
1130                        project_name = value
1131                elif opt in ['-t', '--ticket_prefix']:
1132                        ticket_prefix = value
1133       
1134        settings = ReadConfig(configfile, project_name)
1135        if not settings.has_key('project'):
1136                print __doc__
1137                print 'No Trac project is defined in the email2trac config file.'
1138                sys.exit(1)
1139       
1140        if component:
1141                settings['component'] = component
1142
1143        # The default prefix for ticket values in email2trac.conf
1144        #
1145        settings['ticket_prefix'] = ticket_prefix
1146       
1147        if settings.has_key('trac_version'):
1148                version = settings['trac_version']
1149        else:
1150                version = trac_default_version
1151
1152
1153        #debug HvB
1154        #print settings
1155
1156        try:
1157                if version == '0.9':
1158                        from trac import attachment
1159                        from trac.env import Environment
1160                        from trac.ticket import Ticket
1161                        from trac.web.href import Href
1162                        from trac import util
1163                        from trac.Notify import TicketNotifyEmail
1164                elif version == '0.10':
1165                        from trac import attachment
1166                        from trac.env import Environment
1167                        from trac.ticket import Ticket
1168                        from trac.web.href import Href
1169                        from trac import util
1170                        #
1171                        # return  util.text.to_unicode(str)
1172                        #
1173                        # see http://projects.edgewall.com/trac/changeset/2799
1174                        from trac.ticket.notification import TicketNotifyEmail
1175                        from trac import config as trac_config
1176                elif version == '0.11':
1177                        from trac import attachment
1178                        from trac.env import Environment
1179                        from trac.ticket import Ticket
1180                        from trac.web.href import Href
1181                        from trac import config as trac_config
1182                        from trac import util
1183                        #
1184                        # return  util.text.to_unicode(str)
1185                        #
1186                        # see http://projects.edgewall.com/trac/changeset/2799
1187                        from trac.ticket.notification import TicketNotifyEmail
1188                else:
1189                        print 'TRAC version %s is not supported' %version
1190                        sys.exit(1)
1191                       
1192                if settings.has_key('enable_syslog'):
1193                        if SYSLOG_AVAILABLE:
1194                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
1195
1196                env = Environment(settings['project'], create=0)
1197                tktparser = TicketEmailParser(env, settings, float(version))
1198                tktparser.parse(sys.stdin)
1199
1200        # Catch all errors ans log to SYSLOG if we have enabled this
1201        # else stdout
1202        #
1203        except Exception, error:
1204                if ENABLE_SYSLOG:
1205                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
1206
1207                        etype, evalue, etb = sys.exc_info()
1208                        for e in traceback.format_exception(etype, evalue, etb):
1209                                syslog.syslog(e)
1210
1211                        syslog.closelog()
1212                else:
1213                        traceback.print_exc()
1214
1215                if m:
1216                        tktparser.save_email_for_debug(m, True)
1217
1218# EOB
Note: See TracBrowser for help on using the repository browser.