source: trunk/email2trac.py.in @ 213

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

email2trac.py.in:

  • fixed some formating problems with trac version 0.11

debian/control:

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