source: trunk/email2trac.py.in @ 204

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

email2trac.py.in:

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