source: trunk/email2trac.py.in @ 223

Last change on this file since 223 was 223, checked in by bas, 15 years ago

email2trac.py.in:

  • fixed a typo error

ChangeLog?:

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