source: trunk/email2trac.py.in @ 340

Last change on this file since 340 was 340, checked in by bas, 14 years ago

renamed attachments to attach_attachments

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 43.8 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
30Authors:
31  Bas van der Vlies <basv@sara.nl>
32  Walter de Jong <walter@sara.nl>
33
34The scripts reads emails from stdin and inserts directly into a Trac database.
35
36How to use
37----------
38 * See https://subtrac.sara.nl/oss/email2trac/
39
40 * Create an config file:
41    [DEFAULT]                        # REQUIRED
42    project      : /data/trac/test   # REQUIRED
43    debug        : 1                 # OPTIONAL, if set print some DEBUG info
44    trac_version : 0.10              # OPTIONAL, default is 0.11
45
46    [jouvin]                         # OPTIONAL project declaration, if set both fields necessary
47    project      : /data/trac/jouvin # use -p|--project jouvin. 
48       
49 * default config file is : /etc/email2trac.conf
50
51 * Commandline opions:
52                -h,--help
53                -f,--file  <configuration file>
54                -n,--dry-run
55                -p, --project <project name>
56                -t, --ticket_prefix <name>
57
58SVN Info:
59        $Id: email2trac.py.in 340 2010-03-24 14:21:34Z bas $
60"""
61import os
62import sys
63import string
64import getopt
65import stat
66import time
67import email
68import email.Iterators
69import email.Header
70import re
71import urllib
72import unicodedata
73from stat import *
74import mimetypes
75import traceback
76
77
78# Will fail where unavailable, e.g. Windows
79#
80try:
81    import syslog
82    SYSLOG_AVAILABLE = True
83except ImportError:
84    SYSLOG_AVAILABLE = False
85
86from datetime import tzinfo, timedelta, datetime
87from trac import config as trac_config
88
89# Some global variables
90#
91trac_default_version = '0.11'
92m = None
93
94# A UTC class needed for trac version 0.11, added by
95# tbaschak at ktc dot mb dot ca
96#
97class UTC(tzinfo):
98        """UTC"""
99        ZERO = timedelta(0)
100        HOUR = timedelta(hours=1)
101       
102        def utcoffset(self, dt):
103                return self.ZERO
104               
105        def tzname(self, dt):
106                return "UTC"
107               
108        def dst(self, dt):
109                return self.ZERO
110
111
112class TicketEmailParser(object):
113        env = None
114        comment = '> '
115   
116        def __init__(self, env, parameters, version):
117                self.env = env
118
119                # Database connection
120                #
121                self.db = None
122
123                # Save parameters
124                #
125                self.parameters = parameters
126
127                # Some useful mail constants
128                #
129                self.email_name = None
130                self.email_addr = None
131                self.email_from = None
132                self.author     = None
133                self.id         = None
134               
135                self.STRIP_CONTENT_TYPES = list()
136
137                self.VERSION = version
138                self.DRY_RUN = parameters['dry_run']
139                self.VERBOSE = parameters['verbose']
140
141                self.get_config = self.env.config.get
142
143                if parameters.has_key('umask'):
144                        os.umask(int(parameters['umask'], 8))
145
146                if parameters.has_key('debug'):
147                        self.DEBUG = int(parameters['debug'])
148                else:
149                        self.DEBUG = 0
150
151                if parameters.has_key('mailto_link'):
152                        self.MAILTO = int(parameters['mailto_link'])
153                        if parameters.has_key('mailto_cc'):
154                                self.MAILTO_CC = parameters['mailto_cc']
155                        else:
156                                self.MAILTO_CC = ''
157                else:
158                        self.MAILTO = 0
159
160                if parameters.has_key('spam_level'):
161                        self.SPAM_LEVEL = int(parameters['spam_level'])
162                else:
163                        self.SPAM_LEVEL = 0
164
165                if parameters.has_key('spam_header'):
166                        self.SPAM_HEADER = parameters['spam_header']
167                else:
168                        self.SPAM_HEADER = 'X-Spam-Score'
169
170                if parameters.has_key('email_quote'):
171                        self.EMAIL_QUOTE = str(parameters['email_quote'])
172                else:   
173                        self.EMAIL_QUOTE = '> '
174
175                if parameters.has_key('email_header'):
176                        self.EMAIL_HEADER = int(parameters['email_header'])
177                else:
178                        self.EMAIL_HEADER = 0
179
180                if parameters.has_key('alternate_notify_template'):
181                        self.notify_template = str(parameters['alternate_notify_template'])
182                else:
183                        self.notify_template = None
184
185                if parameters.has_key('alternate_notify_template_update'):
186                        self.notify_template_update = str(parameters['alternate_notify_template_update'])
187                else:
188                        self.notify_template_update = None
189
190                if parameters.has_key('reply_all'):
191                        self.REPLY_ALL = int(parameters['reply_all'])
192                else:
193                        self.REPLY_ALL = 0
194
195                if parameters.has_key('ticket_update'):
196                        self.TICKET_UPDATE = int(parameters['ticket_update'])
197                else:
198                        self.TICKET_UPDATE = 0
199
200                if parameters.has_key('drop_spam'):
201                        self.DROP_SPAM = int(parameters['drop_spam'])
202                else:
203                        self.DROP_SPAM = 0
204
205                if parameters.has_key('verbatim_format'):
206                        self.VERBATIM_FORMAT = int(parameters['verbatim_format'])
207                else:
208                        self.VERBATIM_FORMAT = 1
209
210                if parameters.has_key('reflow'):
211                        self.REFLOW = int(parameters['reflow'])
212                else:
213                        self.REFLOW = 1
214
215                if parameters.has_key('drop_alternative_html_version'):
216                        self.DROP_ALTERNATIVE_HTML_VERSION = int(parameters['drop_alternative_html_version'])
217                else:
218                        self.DROP_ALTERNATIVE_HTML_VERSION = 0
219
220                if parameters.has_key('strip_signature'):
221                        self.STRIP_SIGNATURE = int(parameters['strip_signature'])
222                else:
223                        self.STRIP_SIGNATURE = 0
224
225                if parameters.has_key('strip_quotes'):
226                        self.STRIP_QUOTES = int(parameters['strip_quotes'])
227                else:
228                        self.STRIP_QUOTES = 0
229
230                self.properties = dict()
231                if parameters.has_key('inline_properties'):
232                        self.INLINE_PROPERTIES = int(parameters['inline_properties'])
233                else:
234                        self.INLINE_PROPERTIES = 0
235
236                if parameters.has_key('use_textwrap'):
237                        self.USE_TEXTWRAP = int(parameters['use_textwrap'])
238                else:
239                        self.USE_TEXTWRAP = 0
240
241                if parameters.has_key('binhex'):
242                        self.STRIP_CONTENT_TYPES.append('application/mac-binhex40')
243
244                if parameters.has_key('applesingle'):
245                        self.STRIP_CONTENT_TYPES.append('application/applefile')
246
247                if parameters.has_key('appledouble'):
248                        self.STRIP_CONTENT_TYPES.append('application/applefile')
249
250                if parameters.has_key('strip_content_types'):
251                        items = parameters['strip_content_types'].split(',')
252                        for item in items:
253                                self.STRIP_CONTENT_TYPES.append(item.strip())
254
255                self.WORKFLOW = None
256                if parameters.has_key('workflow'):
257                        self.WORKFLOW = parameters['workflow']
258
259                # Use OS independend functions
260                #
261                self.TMPDIR = os.path.normcase('/tmp')
262                if parameters.has_key('tmpdir'):
263                        self.TMPDIR = os.path.normcase(str(parameters['tmpdir']))
264
265                if parameters.has_key('ignore_trac_user_settings'):
266                        self.IGNORE_TRAC_USER_SETTINGS = int(parameters['ignore_trac_user_settings'])
267                else:
268                        self.IGNORE_TRAC_USER_SETTINGS = 0
269
270                if parameters.has_key('email_triggers_workflow'):
271                        self.EMAIL_TRIGGERS_WORKFLOW = int(parameters['email_triggers_workflow'])
272                else:
273                        self.EMAIL_TRIGGERS_WORKFLOW = 1
274
275                if parameters.has_key('subject_field_separator'):
276                        self.SUBJECT_FIELD_SEPARATOR = parameters['subject_field_separator'].strip()
277                else:
278                        self.SUBJECT_FIELD_SEPARATOR = '&'
279
280                self.trac_smtp_from = self.get_config('notification', 'smtp_from')
281
282########## Email Header Field Functions ###########################################################
283
284        def spam(self, message):
285                """
286                # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
287                # Note if Spam_level then '*' are included
288                """
289                spam = False
290                if message.has_key(self.SPAM_HEADER):
291                        spam_l = string.split(message[self.SPAM_HEADER])
292
293                        try:
294                                number = spam_l[0].count('*')
295                        except IndexError, detail:
296                                number = 0
297                               
298                        if number >= self.SPAM_LEVEL:
299                                spam = True
300                               
301                # treat virus mails as spam
302                #
303                elif message.has_key('X-Virus-found'):                 
304                        spam = True
305
306                # How to handle SPAM messages
307                #
308                if self.DROP_SPAM and spam:
309                        if self.DEBUG > 2 :
310                                print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
311
312                        return 'drop'   
313
314                elif spam:
315
316                        return 'Spam'   
317
318                else:
319
320                        return False
321
322        def email_header_acl(self, keyword, header_field, default):
323                """
324                This function wil check if the email address is allowed or denied
325                to send mail to the ticket list
326            """
327                try:
328                        mail_addresses = self.parameters[keyword]
329
330                        # Check if we have an empty string
331                        #
332                        if not mail_addresses:
333                                return default
334
335                except KeyError, detail:
336                        if self.DEBUG > 2 :
337                                print 'TD: %s not defined, all messages are allowed.' %(keyword)
338
339                        return default
340
341                mail_addresses = string.split(mail_addresses, ',')
342
343                for entry in mail_addresses:
344                        entry = entry.strip()
345                        TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
346                        result =  TO_RE.search(header_field)
347                        if result:
348                                return True
349
350                return False
351
352        def email_to_unicode(self, message_str):
353                """
354                Email has 7 bit ASCII code, convert it to unicode with the charset
355        that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
356                understands it.
357                """
358                if self.VERBOSE:
359                        print "VB: email_to_unicode"
360
361                results =  email.Header.decode_header(message_str)
362
363                s = None
364                for text,format in results:
365                        if format:
366                                try:
367                                        temp = unicode(text, format)
368                                except UnicodeError, detail:
369                                        # This always works
370                                        #
371                                        temp = unicode(text, 'iso-8859-15')
372                                except LookupError, detail:
373                                        #text = 'ERROR: Could not find charset: %s, please install' %format
374                                        #temp = unicode(text, 'iso-8859-15')
375                                        temp = message_str
376                                       
377                        else:
378                                temp = string.strip(text)
379                                temp = unicode(text, 'iso-8859-15')
380
381                        if s:
382                                s = '%s %s' %(s, temp)
383                        else:
384                                s = '%s' %temp
385
386                #s = s.encode('utf-8')
387                return s
388
389
390        def email_header_txt(self, m):
391                """
392                Display To and CC addresses in description field
393                """
394                s = ''
395
396                if m['To'] and len(m['To']) > 0:
397                        s = "'''To:''' %s\r\n" %(m['To'])
398                if m['Cc'] and len(m['Cc']) > 0:
399                        s = "%s'''Cc:''' %s\r\n" % (s, m['Cc'])
400
401                return  self.email_to_unicode(s)
402
403
404        def get_sender_info(self, message):
405                """
406                Get the default author name and email address from the message
407                """
408
409                self.email_to = self.email_to_unicode(message['to'])
410                self.to_name, self.to_email_addr = email.Utils.parseaddr (self.email_to)
411
412                self.email_from = self.email_to_unicode(message['from'])
413                self.email_name, self.email_addr  = email.Utils.parseaddr(self.email_from)
414
415                ## Trac can not handle author's name that contains spaces
416                #  and forbid the ticket email address as author field
417
418                if self.email_addr == self.trac_smtp_from:
419                        self.author = "email2trac"
420                else:
421                        self.author = self.email_addr
422
423                if self.IGNORE_TRAC_USER_SETTINGS:
424                        return
425
426                # Is this a registered user, use email address as search key:
427                # result:
428                #   u : login name
429                #   n : Name that the user has set in the settings tab
430                #   e : email address that the user has set in the settings tab
431                #
432                users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
433                        if e and (e.lower() == self.email_addr.lower()) ]
434
435                if len(users) == 1:
436                        self.email_from = users[0][0]
437                        self.author = 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                if self.DEBUG:
444                        print 'TD: set_reply_fields'
445
446                ## Only use name or email adress
447                #ticket['reporter'] = self.email_from
448                ticket['reporter'] = self.author
449
450
451                # Put all CC-addresses in ticket CC field
452                #
453                if self.REPLY_ALL:
454
455                        email_cc = ''
456
457                        cc_addrs = email.Utils.getaddresses( message.get_all('cc', []) )
458
459                        if not cc_addrs:
460                                return
461
462                        ## Build a list of forbidden CC addresses
463                        #
464                        #to_addrs = email.Utils.getaddresses( message.get_all('to', []) )
465                        #to_list = list()
466                        #for n,e in to_addrs:
467                        #       to_list.append(e)
468                               
469                        # Remove reporter email address if notification is
470                        # on
471                        #
472                        if self.notification:
473                                try:
474                                        cc_addrs.remove((self.author, self.email_addr))
475                                except ValueError, detail:
476                                        pass
477
478                        for name,addr in cc_addrs:
479               
480                                ## Prevent mail loop
481                                #
482                                #if addr in to_list:
483
484                                if addr == self.trac_smtp_from:
485                                        if self.DEBUG:
486                                                print "Skipping %s mail address for CC-field" %(addr)
487                                        continue
488
489                                if email_cc:
490                                        email_cc = '%s, %s' %(email_cc, addr)
491                                else:
492                                        email_cc = addr
493
494                        if email_cc:
495                                if self.DEBUG:
496                                        print 'TD: set_reply_fields: %s' %email_cc
497
498                                ticket['cc'] = self.email_to_unicode(email_cc)
499
500
501########## DEBUG functions  ###########################################################
502
503        def debug_body(self, message_body, tempfile=False):
504                if tempfile:
505                        import tempfile
506                        body_file = tempfile.mktemp('.email2trac')
507                else:
508                        body_file = os.path.join(self.TMPDIR, 'body.txt')
509
510                if self.DRY_RUN:
511                        print 'DRY-RUN: not saving body to %s' %(body_file)
512                        return
513
514                print 'TD: writing body to %s' %(body_file)
515                fx = open(body_file, 'wb')
516                if not message_body:
517                                message_body = '(None)'
518
519                message_body = message_body.encode('utf-8')
520                #message_body = unicode(message_body, 'iso-8859-15')
521
522                fx.write(message_body)
523                fx.close()
524                try:
525                        os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
526                except OSError:
527                        pass
528
529        def debug_attachments(self, message_parts):
530                """
531                """
532                if self.VERBOSE:
533                        print "VB: debug_attachments"
534               
535                n = 0
536                for item in message_parts:
537                        # Skip inline text parts
538                        if not isinstance(item, tuple):
539                                continue
540                               
541                        (original, filename, part) = item
542
543                        n = n + 1
544                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
545                        print 'TD: part%d: filename: %s' % (n, part.get_filename())
546
547                        part_file = os.path.join(self.TMPDIR, part.get_filename())
548                        print 'TD: writing part%d (%s)' % (n,part_file)
549
550                        if self.DRY_RUN:
551                                print 'DRY_RUN: NOT saving attachments'
552                                continue
553
554                        fx = open(part_file, 'wb')
555                        text = part.get_payload(decode=1)
556
557                        if not text:
558                                text = '(None)'
559
560                        fx.write(text)
561                        fx.close()
562
563                        try:
564                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
565                        except OSError:
566                                pass
567
568        def save_email_for_debug(self, message, tempfile=False):
569
570                if tempfile:
571                        import tempfile
572                        msg_file = tempfile.mktemp('.email2trac')
573                else:
574                        #msg_file = '/var/tmp/msg.txt'
575                        msg_file = os.path.join(self.TMPDIR, 'msg.txt')
576
577                if self.DRY_RUN:
578                        print 'DRY_RUN: NOT saving email message to %s' %(msg_file)
579                else:
580                        print 'TD: saving email to %s' %(msg_file)
581
582                        fx = open(msg_file, 'wb')
583                        fx.write('%s' % message)
584                        fx.close()
585                       
586                        try:
587                                os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
588                        except OSError:
589                                pass
590
591                message_parts = self.get_message_parts(message)
592                message_parts = self.unique_attachment_names(message_parts)
593                body_text = self.body_text(message_parts)
594                self.debug_body(body_text, True)
595                self.debug_attachments(message_parts)
596
597########## Conversion functions  ###########################################################
598
599        def str_to_dict(self, s):
600                """
601                Transfrom a string of the form [<key>=<value>]+ to dict[<key>] = <value>
602                """
603
604                fields = string.split(s, self.SUBJECT_FIELD_SEPARATOR)
605
606                result = dict()
607                for field in fields:
608                        try:
609                                index, value = string.split(field, '=')
610
611                                # We can not change the description of a ticket via the subject
612                                # line. The description is the body of the email
613                                #
614                                if index.lower() in ['description']:
615                                        continue
616
617                                if value:
618                                        result[index.lower()] = value
619
620                        except ValueError:
621                                pass
622                return result
623
624########## TRAC ticket functions  ###########################################################
625
626        def update_ticket_fields(self, ticket, user_dict, use_default=None):
627                """
628                This will update the ticket fields. It will check if the
629                given fields are known and if the right values are specified
630                It will only update the ticket field value:
631                        - If the field is known
632                        - If the value supplied is valid for the ticket field.
633                          If not then there are two options:
634                           1) Skip the value (use_default=None)
635                           2) Set default value for field (use_default=1)
636                """
637                if self.VERBOSE:
638                        print "VB: update_ticket_fields"
639
640                # Build a system dictionary from the ticket fields
641                # with field as index and option as value
642                #
643                sys_dict = dict()
644                for field in ticket.fields:
645                        try:
646                                sys_dict[field['name']] = field['options']
647
648                        except KeyError:
649                                sys_dict[field['name']] = None
650                                pass
651
652                ## Check user supplied fields an compare them with the
653                # system one's
654                #
655                for field,value in user_dict.items():
656                        if self.DEBUG >= 10:
657                                print  'user_field\t %s = %s' %(field,value)
658
659                        ## To prevent mail loop
660                        #
661                        if field == 'cc':
662
663                                cc_list = user_dict['cc'].split(',')
664
665                                if self.trac_smtp_from in cc_list:
666                                        if self.DEBUG > 10:
667                                                print 'TD: MAIL LOOP: %s is not allowed as CC address' %(self.trac_smtp_from)
668                                        cc_list.remove(self.trac_smtp_from)
669
670                                value = ','.join(cc_list)
671                               
672
673                        if sys_dict.has_key(field):
674
675                                # Check if value is an allowed system option, if TypeError then
676                                # every value is allowed
677                                #
678                                try:
679                                        if value in sys_dict[field]:
680                                                ticket[field] = value
681                                        else:
682                                                # Must we set a default if value is not allowed
683                                                #
684                                                if use_default:
685                                                        value = self.get_config('ticket', 'default_%s' %(field) )
686                                                        ticket[field] = value
687
688                                except TypeError:
689                                        ticket[field] = value
690
691                                if self.DEBUG >= 10:
692                                        print  'ticket_field\t %s = %s' %(field,  ticket[field])
693                                       
694        def ticket_update(self, m, id, spam):
695                """
696                If the current email is a reply to an existing ticket, this function
697                will append the contents of this email to that ticket, instead of
698                creating a new one.
699                """
700                if self.VERBOSE:
701                        print "VB: ticket_update: %s" %id
702
703                # Must we update ticket fields
704                #
705                update_fields = dict()
706                try:
707                        id, keywords = string.split(id, '?')
708
709                        # Skip the last ':' character
710                        #
711                        keywords = keywords[:-1]
712                        update_fields = self.str_to_dict(keywords)
713
714                        # Strip '#'
715                        #
716                        self.id = int(id[1:])
717
718                except ValueError:
719                        # Strip '#' and ':'
720                        #
721                        self.id = int(id[1:-1])
722
723
724                # When is the change committed
725                #
726                if self.VERSION == 0.11:
727                        utc = UTC()
728                        when = datetime.now(utc)
729                else:
730                        when = int(time.time())
731
732                try:
733                        tkt = Ticket(self.env, self.id, self.db)
734                except util.TracError, detail:
735                        # Not a valid ticket
736                        self.id = None
737                        return False
738
739                # How many changes has this ticket
740                cnum = len(tkt.get_changelog())
741
742
743                # reopen the ticket if it is was closed
744                # We must use the ticket workflow framework
745                #
746                if tkt['status'] in ['closed'] and self.EMAIL_TRIGGERS_WORKFLOW:
747
748                        #print controller.actions['reopen']
749                        #
750                        # As reference 
751                        # req = Mock(href=Href('/'), abs_href=Href('http://www.example.com/'), authname='anonymous', perm=MockPerm(), args={})
752                        #
753                        #a = controller.render_ticket_action_control(req, tkt, 'reopen')
754                        #print 'controller : ', a
755                        #
756                        #b = controller.get_all_status()
757                        #print 'get all status: ', b
758                        #
759                        #b = controller.get_ticket_changes(req, tkt, 'reopen')
760                        #print 'get_ticket_changes :', b
761
762                        if self.WORKFLOW and (self.VERSION in [0.11]) :
763                                from trac.ticket.default_workflow import ConfigurableTicketWorkflow
764                                from trac.test import Mock, MockPerm
765
766                                req = Mock(authname='anonymous', perm=MockPerm(), args={})
767
768                                controller = ConfigurableTicketWorkflow(self.env)
769                                fields = controller.get_ticket_changes(req, tkt, self.WORKFLOW)
770
771                                if self.DEBUG:
772                                        print 'TD: Workflow ticket update fields: ', fields
773
774                                for key in fields.keys():
775                                        tkt[key] = fields[key]
776
777                        else:
778                                tkt['status'] = 'reopened'
779                                tkt['resolution'] = ''
780
781                # Must we update some ticket fields properties via subjectline
782                #
783                if update_fields:
784                        self.update_ticket_fields(tkt, update_fields)
785
786                message_parts = self.get_message_parts(m)
787                message_parts = self.unique_attachment_names(message_parts)
788
789                # Must we update some ticket fields properties via body_text
790                #
791                if self.properties:
792                                self.update_ticket_fields(tkt, self.properties)
793
794                if self.EMAIL_HEADER:
795                        message_parts.insert(0, self.email_header_txt(m))
796
797                body_text = self.body_text(message_parts)
798
799                if body_text.strip() or update_fields or self.properties:
800                        if self.DRY_RUN:
801                                print 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) ', self.author, cnum
802                        else:
803                                tkt.save_changes(self.author, body_text, when, None, str(cnum))
804                       
805
806                if self.VERSION  == 0.9:
807                        error_with_attachments = self.attach_attachments(message_parts, True)
808                else:
809                        error_with_attachments = self.attach_attachments(message_parts)
810
811                if self.notification and not spam:
812                        self.notify(tkt, False, when)
813
814                return True
815
816        def set_ticket_fields(self, ticket):
817                """
818                set the ticket fields to value specified
819                        - /etc/email2trac.conf with <prefix>_<field>
820                        - trac default values, trac.ini
821                """
822                user_dict = dict()
823
824                for field in ticket.fields:
825
826                        name = field['name']
827
828                        ## skip some fields like resolution
829                        #
830                        if name in [ 'resolution' ]:
831                                continue
832
833                        ## default trac value
834                        #
835                        if not field.get('custom'):
836                                value = self.get_config('ticket', 'default_%s' %(name) )
837                        else:
838                                value = field.get('value')
839                                options = field.get('options')
840                                if value and options and (value not in options):
841                                        value = options[int(value)]
842
843                        if self.DEBUG > 10:
844                                print 'trac.ini name %s = %s' %(name, value)
845
846                        ## email2trac.conf settings
847                        #
848                        prefix = self.parameters['ticket_prefix']
849                        try:
850                                value = self.parameters['%s_%s' %(prefix, name)]
851                                if self.DEBUG > 10:
852                                        print 'email2trac.conf %s = %s ' %(name, value)
853
854                        except KeyError, detail:
855                                pass
856               
857                        if self.DEBUG:
858                                print 'user_dict[%s] = %s' %(name, value)
859
860                        user_dict[name] = value
861
862                self.update_ticket_fields(ticket, user_dict, use_default=1)
863
864                ## Set status ticket
865                #
866                ticket['status'] = 'new'
867
868
869
870        def new_ticket(self, msg, subject, spam, set_fields = None):
871                """
872                Create a new ticket
873                """
874                if self.DEBUG:
875                        print "TD: new_ticket"
876
877                tkt = Ticket(self.env)
878
879                self.set_reply_fields(tkt, msg)
880
881                self.set_ticket_fields(tkt)
882
883                # Old style setting for component, will be removed
884                #
885                if spam:
886                        tkt['component'] = 'Spam'
887
888                elif self.parameters.has_key('component'):
889                        tkt['component'] = self.parameters['component']
890
891                if not msg['Subject']:
892                        tkt['summary'] = u'(No subject)'
893                else:
894                        tkt['summary'] = subject
895
896
897                if set_fields:
898                        rest, keywords = string.split(set_fields, '?')
899
900                        if keywords:
901                                update_fields = self.str_to_dict(keywords)
902                                self.update_ticket_fields(tkt, update_fields)
903
904                # produce e-mail like header
905                #
906                head = ''
907                if self.EMAIL_HEADER > 0:
908                        head = self.email_header_txt(msg)
909
910                message_parts = self.get_message_parts(msg)
911
912                # Must we update some ticket fields properties via body_text
913                #
914                if self.properties:
915                                self.update_ticket_fields(tkt, self.properties)
916
917                if self.DEBUG:
918                        print 'TD: self.get_message_parts ',
919                        print message_parts
920
921                message_parts = self.unique_attachment_names(message_parts)
922                if self.DEBUG:
923                        print 'TD: self.unique_attachment_names',
924                        print message_parts
925               
926                if self.EMAIL_HEADER > 0:
927                        message_parts.insert(0, self.email_header_txt(msg))
928                       
929                body_text = self.body_text(message_parts)
930
931                tkt['description'] = body_text
932
933                #when = int(time.time())
934                #
935                utc = UTC()
936                when = datetime.now(utc)
937
938                if not self.DRY_RUN:
939                        self.id = tkt.insert()
940       
941                changed = False
942                comment = ''
943
944                # some routines in trac are dependend on ticket id     
945                # like alternate notify template
946                #
947                if self.notify_template:
948                        tkt['id'] = self.id
949                        changed = True
950
951                ## Rewrite the description if we have mailto enabled
952                #
953                if self.MAILTO:
954                        changed = True
955                        comment = u'\nadded mailto line\n'
956                        mailto = self.html_mailto_link( m['Subject'], body_text)
957
958                        tkt['description'] = u'%s\r\n%s%s\r\n' \
959                                %(head, mailto, body_text)
960       
961                ## Save the attachments to the ticket   
962                #
963                error_with_attachments =  self.attach_attachments(message_parts)
964
965                if error_with_attachments:
966                        changed = True
967                        comment = '%s\n%s\n' %(comment, error_with_attachments)
968
969                if changed:
970                        if self.DRY_RUN:
971                                print 'DRY_RUN: tkt.save_changes(self.author, comment) ', self.author
972                        else:
973                                tkt.save_changes(self.author, comment)
974                                #print tkt.get_changelog(self.db, when)
975
976                if self.notification and not spam:
977                        self.notify(tkt, True)
978
979
980########## Fullblog functions  ###########################################################
981
982        def blog(self, id):
983                """
984                The blog create/update function
985                """
986                # import the modules
987                #
988                from tracfullblog.core import FullBlogCore
989                from tracfullblog.model import BlogPost, BlogComment
990                from trac.test import Mock, MockPerm
991
992                # instantiate blog core
993                blog = FullBlogCore(self.env)
994                req = Mock(authname='anonymous', perm=MockPerm(), args={})
995
996                if id:
997
998                        # update blog
999                        #
1000                        comment = BlogComment(self.env, id)
1001                        comment.author = self.author
1002
1003                        message_parts = self.get_message_parts(m)
1004                        comment.comment = self.body_text(message_parts)
1005
1006                        blog.create_comment(req, comment)
1007
1008                else:
1009                        # create blog
1010                        #
1011                        import time
1012                        post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
1013
1014                        #post = BlogPost(self.env, blog._get_default_postname(self.env))
1015                       
1016                        post.author = self.author
1017                        post.title = self.email_to_unicode(m['Subject'])
1018
1019                        message_parts = self.get_message_parts(m)
1020                        post.body = self.body_text(message_parts)
1021                       
1022                        blog.create_post(req, post, self.author, u'Created by email2trac', False)
1023
1024
1025        def parse(self, fp):
1026                global m
1027
1028                m = email.message_from_file(fp)
1029               
1030                if not m:
1031                        if self.DEBUG:
1032                                print "TD: This is not a valid email message format"
1033                        return
1034                       
1035                # Work around lack of header folding in Python; see http://bugs.python.org/issue4696
1036                try:
1037                        m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
1038                except AttributeError, detail:
1039                        pass
1040
1041                if self.DEBUG > 1:        # save the entire e-mail message text
1042                        self.save_email_for_debug(m, True)
1043
1044                self.db = self.env.get_db_cnx()
1045                self.get_sender_info(m)
1046
1047                if not self.email_header_acl('white_list', self.email_addr, True):
1048                        if self.DEBUG > 1 :
1049                                print 'Message rejected : %s not in white list' %(self.email_addr)
1050                        return False
1051
1052                if self.email_header_acl('black_list', self.email_addr, False):
1053                        if self.DEBUG > 1 :
1054                                print 'Message rejected : %s in black list' %(self.email_addr)
1055                        return False
1056
1057                if not self.email_header_acl('recipient_list', self.to_email_addr, True):
1058                        if self.DEBUG > 1 :
1059                                print 'Message rejected : %s not in recipient list' %(self.to_email_addr)
1060                        return False
1061
1062                # If drop the message
1063                #
1064                if self.spam(m) == 'drop':
1065                        return False
1066
1067                elif self.spam(m) == 'spam':
1068                        spam_msg = True
1069                else:
1070                        spam_msg = False
1071
1072                if self.get_config('notification', 'smtp_enabled') in ['true']:
1073                        self.notification = 1
1074                else:
1075                        self.notification = 0
1076
1077
1078                # Check if  FullBlogPlugin is installed
1079                #
1080                blog_enabled = None
1081                if self.get_config('components', 'tracfullblog.*') in ['enabled']:
1082                        blog_enabled = True
1083
1084                if not m['Subject']:
1085                        subject  = 'No Subject'
1086                else:
1087                        subject  = self.email_to_unicode(m['Subject'])         
1088
1089                #
1090                # [hic] #1529: Re: LRZ
1091                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
1092                #
1093                TICKET_RE = re.compile(r"""
1094                        (?P<blog>blog:(?P<blog_id>\w*))
1095                        |(?P<new_fields>[#][?].*)
1096                        |(?P<reply>[#][\d]+:)
1097                        |(?P<reply_fields>[#][\d]+\?.*?:)
1098                        """, re.VERBOSE)
1099
1100                # Find out if this is a ticket or a blog
1101                #
1102                result =  TICKET_RE.search(subject)
1103
1104                if result:
1105                        if result.group('blog'):
1106                                if blog_enabled:
1107                                        self.blog(result.group('blog_id'))
1108                                else:
1109                                        if self.DEBUG:
1110                                                print "Fullblog plugin is not installed"
1111                                                return
1112
1113                        # update ticket + fields
1114                        #
1115                        if result.group('reply_fields') and self.TICKET_UPDATE:
1116                                self.ticket_update(m, result.group('reply_fields'), spam_msg)
1117
1118                        # Update ticket
1119                        #
1120                        elif result.group('reply') and self.TICKET_UPDATE:
1121                                self.ticket_update(m, result.group('reply'), spam_msg)
1122
1123                        # New ticket + fields
1124                        #
1125                        elif result.group('new_fields'):
1126                                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
1127
1128                # Create ticket
1129                #
1130                else:
1131                        self.new_ticket(m, subject, spam_msg)
1132
1133        def strip_signature(self, text):
1134                """
1135                Strip signature from message, inspired by Mailman software
1136                """
1137                body = []
1138                for line in text.splitlines():
1139                        if line == '-- ':
1140                                break
1141                        body.append(line)
1142
1143                return ('\n'.join(body))
1144
1145        def reflow(self, text, delsp = 0):
1146                """
1147                Reflow the message based on the format="flowed" specification (RFC 3676)
1148                """
1149                flowedlines = []
1150                quotelevel = 0
1151                prevflowed = 0
1152
1153                for line in text.splitlines():
1154                        from re import match
1155                       
1156                        # Figure out the quote level and the content of the current line
1157                        m = match('(>*)( ?)(.*)', line)
1158                        linequotelevel = len(m.group(1))
1159                        line = m.group(3)
1160
1161                        # Determine whether this line is flowed
1162                        if line and line != '-- ' and line[-1] == ' ':
1163                                flowed = 1
1164                        else:
1165                                flowed = 0
1166
1167                        if flowed and delsp and line and line[-1] == ' ':
1168                                line = line[:-1]
1169
1170                        # If the previous line is flowed, append this line to it
1171                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1172                                flowedlines[-1] += line
1173                        # Otherwise, start a new line
1174                        else:
1175                                flowedlines.append('>' * linequotelevel + line)
1176
1177                        prevflowed = flowed
1178                       
1179
1180                return '\n'.join(flowedlines)
1181
1182        def strip_quotes(self, text):
1183                """
1184                Strip quotes from message by Nicolas Mendoza
1185                """
1186                body = []
1187                for line in text.splitlines():
1188                        if line.startswith(self.EMAIL_QUOTE):
1189                                continue
1190                        body.append(line)
1191
1192                return ('\n'.join(body))
1193
1194        def inline_properties(self, text):
1195                """
1196                Parse text if we use inline keywords to set ticket fields
1197                """
1198                if self.DEBUG:
1199                        print 'TD: inline_properties function'
1200
1201                properties = dict()
1202                body = list()
1203
1204                INLINE_EXP = re.compile('\s*[@]\s*([a-zA-Z]+)\s*:(.*)$')
1205
1206                for line in text.splitlines():
1207                        match = INLINE_EXP.match(line)
1208                        if match:
1209                                keyword, value = match.groups()
1210                                self.properties[keyword] = value.strip()
1211                                if self.DEBUG:
1212                                        print "TD: inline properties: %s : %s" %(keyword,value)
1213                        else:
1214                                body.append(line)
1215                               
1216                return '\n'.join(body)
1217
1218
1219        def wrap_text(self, text, replace_whitespace = False):
1220                """
1221                Will break a lines longer then given length into several small
1222                lines of size given length
1223                """
1224                import textwrap
1225
1226                LINESEPARATOR = '\n'
1227                reformat = ''
1228
1229                for s in text.split(LINESEPARATOR):
1230                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
1231                        if tmp:
1232                                reformat = '%s\n%s' %(reformat,tmp)
1233                        else:
1234                                reformat = '%s\n' %reformat
1235
1236                return reformat
1237
1238                # Python2.4 and higher
1239                #
1240                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1241                #
1242
1243        def inline_part(self, part):
1244                """
1245                """
1246                if self.VERBOSE:
1247                        print "VB: inline_part()"
1248
1249                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1250
1251        def get_message_parts(self, msg):
1252                """
1253                parses the email message and returns a list of body parts and attachments
1254                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
1255                """
1256                if self.VERBOSE:
1257                        print "VB: get_message_parts()"
1258
1259                message_parts = list()
1260       
1261                ALTERNATIVE_MULTIPART = False
1262
1263                for part in msg.walk():
1264                        if self.DEBUG:
1265                                print 'TD: Message part: Main-Type: %s' % part.get_content_maintype()
1266                                print 'TD: Message part: Content-Type: %s' % part.get_content_type()
1267
1268                        ## Check content type
1269                        #
1270                        if part.get_content_type() in self.STRIP_CONTENT_TYPES:
1271
1272                                if self.DEBUG:
1273                                        print "TD: A %s attachment named '%s' was skipped" %(part.get_content_type(), part.get_filename())
1274
1275                                continue
1276
1277                        ## Catch some mulitpart execptions
1278                        #
1279                        if part.get_content_type() == 'multipart/alternative':
1280                                ALTERNATIVE_MULTIPART = True
1281                                continue
1282
1283                        ## Skip multipart containers
1284                        #
1285                        if part.get_content_maintype() == 'multipart':
1286                                if self.DEBUG:
1287                                        print "TD: Skipping multipart container"
1288                                continue
1289                       
1290                        ## Check if this is an inline part. It's inline if there is co Cont-Disp header, or if there is one and it says "inline"
1291                        #
1292                        inline = self.inline_part(part)
1293
1294                        ## Drop HTML message
1295                        #
1296                        if ALTERNATIVE_MULTIPART and self.DROP_ALTERNATIVE_HTML_VERSION:
1297                                if part.get_content_type() == 'text/html':
1298                                        if self.DEBUG:
1299                                                print "TD: Skipping alternative HTML message"
1300
1301                                        ALTERNATIVE_MULTIPART = False
1302                                        continue
1303
1304                        ## Inline text parts are where the body is
1305                        #
1306                        if part.get_content_type() == 'text/plain' and inline:
1307                                if self.DEBUG:
1308                                        print 'TD:               Inline body part'
1309
1310                                # Try to decode, if fails then do not decode
1311                                #
1312                                body_text = part.get_payload(decode=1)
1313                                if not body_text:                       
1314                                        body_text = part.get_payload(decode=0)
1315
1316                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1317                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
1318
1319                                if self.REFLOW and not self.VERBATIM_FORMAT and format == 'flowed':
1320                                        body_text = self.reflow(body_text, delsp == 'yes')
1321       
1322                                if self.STRIP_SIGNATURE:
1323                                        body_text = self.strip_signature(body_text)
1324
1325                                if self.STRIP_QUOTES:
1326                                        body_text = self.strip_quotes(body_text)
1327
1328                                if self.INLINE_PROPERTIES:
1329                                        body_text = self.inline_properties(body_text)
1330
1331                                if self.USE_TEXTWRAP:
1332                                        body_text = self.wrap_text(body_text)
1333
1334                                ## Get contents charset (iso-8859-15 if not defined in mail headers)
1335                                #
1336                                charset = part.get_content_charset()
1337                                if not charset:
1338                                        charset = 'iso-8859-15'
1339
1340                                try:
1341                                        ubody_text = unicode(body_text, charset)
1342
1343                                except UnicodeError, detail:
1344                                        ubody_text = unicode(body_text, 'iso-8859-15')
1345
1346                                except LookupError, detail:
1347                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
1348
1349                                if self.VERBATIM_FORMAT:
1350                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1351                                else:
1352                                        message_parts.append('%s' %ubody_text)
1353                        else:
1354                                if self.DEBUG:
1355                                        try:
1356                                                print 'TD:               Filename: %s' % part.get_filename()
1357                                        except UnicodeEncodeError, detail:
1358                                                print 'TD:               Filename: Can not be printed due to non-ascii characters'
1359
1360                                ## Convert 7-bit filename to 8 bits value
1361                                #
1362                                filename = self.email_to_unicode(part.get_filename())
1363                                message_parts.append((filename, part))
1364
1365                return message_parts
1366               
1367        def unique_attachment_names(self, message_parts):
1368                """
1369                """
1370                renamed_parts = []
1371                attachment_names = set()
1372
1373                for item in message_parts:
1374                       
1375                        ## If not an attachment, leave it alone
1376                        #
1377                        if not isinstance(item, tuple):
1378                                renamed_parts.append(item)
1379                                continue
1380                               
1381                        (filename, part) = item
1382
1383                        ## If no filename, use a default one
1384                        #
1385                        if not filename:
1386                                filename = 'untitled-part'
1387
1388                                # Guess the extension from the content type, use non strict mode
1389                                # some additional non-standard but commonly used MIME types
1390                                # are also recognized
1391                                #
1392                                ext = mimetypes.guess_extension(part.get_content_type(), False)
1393                                if not ext:
1394                                        ext = '.bin'
1395
1396                                filename = '%s%s' % (filename, ext)
1397
1398# We now use the attachment insert function
1399#
1400                        ## Discard relative paths in attachment names
1401                        #
1402                        #filename = filename.replace('\\', '/').replace(':', '/')
1403                        #filename = os.path.basename(filename)
1404                        #
1405                        # We try to normalize the filename to utf-8 NFC if we can.
1406                        # Files uploaded from OS X might be in NFD.
1407                        # Check python version and then try it
1408                        #
1409                        #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1410                        #       try:
1411                        #               filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1412                        #       except TypeError:
1413                        #               pass
1414
1415                        # Make the filename unique for this ticket
1416                        num = 0
1417                        unique_filename = filename
1418                        dummy_filename, ext = os.path.splitext(filename)
1419
1420                        while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
1421                                num += 1
1422                                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
1423                               
1424                        if self.DEBUG:
1425                                try:
1426                                        print 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
1427                                except UnicodeEncodeError, detail:
1428                                        print 'Filename can not be printed due to non-ascii characters'
1429
1430                        attachment_names.add(unique_filename)
1431
1432                        renamed_parts.append((filename, unique_filename, part))
1433       
1434                return renamed_parts
1435                       
1436                       
1437        def attachment_exists(self, filename):
1438
1439                if self.DEBUG:
1440                        s = 'TD: attachment already exists: Ticket id : '
1441                        try:
1442                                print "%s%s, Filename : %s" %(s, self.id, filename)
1443                        except UnicodeEncodeError, detail:
1444                                print "%s%s, Filename : Can not be printed due to non-ascii characters" %(s, self.id)
1445
1446                # We have no valid ticket id
1447                #
1448                if not self.id:
1449                        return False
1450
1451                try:
1452                        att = attachment.Attachment(self.env, 'ticket', self.id, filename)
1453                        return True
1454                except attachment.ResourceNotFound:
1455                        return False
1456                       
1457        def body_text(self, message_parts):
1458                body_text = []
1459               
1460                for part in message_parts:
1461                        # Plain text part, append it
1462                        if not isinstance(part, tuple):
1463                                body_text.extend(part.strip().splitlines())
1464                                body_text.append("")
1465                                continue
1466                               
1467                        (original, filename, part) = part
1468                        inline = self.inline_part(part)
1469                       
1470                        if part.get_content_maintype() == 'image' and inline:
1471                                body_text.append('[[Image(%s)]]' % filename)
1472                                body_text.append("")
1473                        else:
1474                                body_text.append('[attachment:"%s"]' % filename)
1475                                body_text.append("")
1476                               
1477                body_text = '\r\n'.join(body_text)
1478                return body_text
1479
1480        def notify(self, tkt, new=True, modtime=0):
1481                """
1482                A wrapper for the TRAC notify function. So we can use templates
1483                """
1484                if self.DRY_RUN:
1485                                print 'DRY_RUN: self.notify(tkt, True) ', self.author
1486                                return
1487                try:
1488                        # create false {abs_}href properties, to trick Notify()
1489                        #
1490                        if not self.VERSION == 0.11:
1491                                self.env.abs_href = Href(self.get_config('project', 'url'))
1492                                self.env.href = Href(self.get_config('project', 'url'))
1493
1494                        tn = TicketNotifyEmail(self.env)
1495
1496                        if self.notify_template:
1497
1498                                if self.VERSION == 0.11:
1499
1500                                        from trac.web.chrome import Chrome
1501
1502                                        if self.notify_template_update and not new:
1503                                                tn.template_name = self.notify_template_update
1504                                        else:
1505                                                tn.template_name = self.notify_template
1506
1507                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
1508                                               
1509                                else:
1510
1511                                        tn.template_name = self.notify_template;
1512
1513                        tn.notify(tkt, new, modtime)
1514
1515                except Exception, e:
1516                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(self.id, e)
1517
1518        def html_mailto_link(self, subject, body):
1519                """
1520                This function returns a HTML mailto tag with the ticket id and author email address
1521                """
1522                if not self.author:
1523                        author = self.email_addr
1524                else:   
1525                        author = self.author
1526
1527                # use urllib to escape the chars
1528                #
1529                s = 'mailto:%s?Subject=%s&Cc=%s' %(
1530                       urllib.quote(self.email_addr),
1531                           urllib.quote('Re: #%s: %s' %(self.id, subject)),
1532                           urllib.quote(self.MAILTO_CC)
1533                           )
1534
1535                s = '\r\n{{{\r\n#!html\r\n<a\r\n href="%s">Reply to: %s\r\n</a>\r\n}}}\r\n' %(s, author)
1536                return s
1537
1538        def attach_attachments(self, message_parts, update=False):
1539                '''
1540                save any attachments as files in the ticket's directory
1541                '''
1542                if self.VERBOSE:
1543                        print "VB: attach_attachments()"
1544
1545                if self.DRY_RUN:
1546                        print "DRY_RUN: no attachments attached to tickets"
1547                        return ''
1548
1549                count = 0
1550
1551                # Get Maxium attachment size
1552                #
1553                max_size = int(self.get_config('attachment', 'max_size'))
1554                status   = None
1555               
1556                for item in message_parts:
1557                        # Skip body parts
1558                        if not isinstance(item, tuple):
1559                                continue
1560                               
1561                        (original, filename, part) = item
1562                        #
1563                        # Must be tuneables HvB
1564                        #
1565                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, filename))
1566                        text = part.get_payload(decode=1)
1567                        if not text:
1568                                text = '(None)'
1569                        fd.write(text)
1570                        fd.close()
1571
1572                        # get the file_size
1573                        #
1574                        stats = os.lstat(path)
1575                        file_size = stats[stat.ST_SIZE]
1576
1577                        # Check if the attachment size is allowed
1578                        #
1579                        if (max_size != -1) and (file_size > max_size):
1580                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1581                                        %(status, original, file_size, max_size)
1582
1583                                os.unlink(path)
1584                                continue
1585                        else:
1586                                count = count + 1
1587                                       
1588                        # Insert the attachment
1589                        #
1590                        fd = open(path, 'rb')
1591                        att = attachment.Attachment(self.env, 'ticket', self.id)
1592
1593                        # This will break the ticket_update system, the body_text is vaporized
1594                        # ;-(
1595                        #
1596                        if not update:
1597                                att.author = self.author
1598                                att.description = self.email_to_unicode('Added by email2trac')
1599
1600                        att.insert(filename, fd, file_size)
1601
1602                        #except  util.TracError, detail:
1603                        #       print detail
1604
1605                        # Remove the created temporary filename
1606                        #
1607                        fd.close()
1608                        os.unlink(path)
1609
1610                ## return error
1611                #
1612                return status
1613
1614
1615def ReadConfig(file, name):
1616        """
1617        Parse the config file
1618        """
1619        if not os.path.isfile(file):
1620                print 'File %s does not exist' %file
1621                sys.exit(1)
1622
1623        config = trac_config.Configuration(file)
1624
1625        # Use given project name else use defaults
1626        #
1627        if name:
1628                sections = config.sections()
1629                if not name in sections:
1630                        print "Not a valid project name: %s" %name
1631                        print "Valid names: %s" %sections
1632                        sys.exit(1)
1633
1634                project =  dict()
1635                for option, value in  config.options(name):
1636                        project[option] = value
1637
1638        else:
1639                # use some trac internals to get the defaults
1640                #
1641                project = config.parser.defaults()
1642
1643        return project
1644
1645
1646if __name__ == '__main__':
1647        # Default config file
1648        #
1649        configfile = '@email2trac_conf@'
1650        project = ''
1651        component = ''
1652        ticket_prefix = 'default'
1653        dry_run = None
1654        verbose = None
1655
1656        ENABLE_SYSLOG = 0
1657
1658
1659        SHORT_OPT = 'chf:np:t:v'
1660        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'verbose']
1661
1662        try:
1663                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
1664        except getopt.error,detail:
1665                print __doc__
1666                print detail
1667                sys.exit(1)
1668       
1669        project_name = None
1670        for opt,value in opts:
1671                if opt in [ '-h', '--help']:
1672                        print __doc__
1673                        sys.exit(0)
1674                elif opt in ['-c', '--component']:
1675                        component = value
1676                elif opt in ['-f', '--file']:
1677                        configfile = value
1678                elif opt in ['-n', '--dry-run']:
1679                        dry_run = True
1680                elif opt in ['-p', '--project']:
1681                        project_name = value
1682                elif opt in ['-t', '--ticket_prefix']:
1683                        ticket_prefix = value
1684                elif opt in ['-v', '--version']:
1685                        verbose = True
1686       
1687        settings = ReadConfig(configfile, project_name)
1688        if not settings.has_key('project'):
1689                print __doc__
1690                print 'No Trac project is defined in the email2trac config file.'
1691                sys.exit(1)
1692       
1693        if component:
1694                settings['component'] = component
1695
1696        # The default prefix for ticket values in email2trac.conf
1697        #
1698        settings['ticket_prefix'] = ticket_prefix
1699        settings['dry_run'] = dry_run
1700        settings['verbose'] = verbose
1701       
1702        if settings.has_key('trac_version'):
1703                version = settings['trac_version']
1704        else:
1705                version = trac_default_version
1706
1707
1708        #debug HvB
1709        #print settings
1710
1711        try:
1712                if version == '0.9':
1713                        from trac import attachment
1714                        from trac.env import Environment
1715                        from trac.ticket import Ticket
1716                        from trac.web.href import Href
1717                        from trac import util
1718                        from trac.Notify import TicketNotifyEmail
1719                elif version == '0.10':
1720                        from trac import attachment
1721                        from trac.env import Environment
1722                        from trac.ticket import Ticket
1723                        from trac.web.href import Href
1724                        from trac import util
1725                        #
1726                        # return  util.text.to_unicode(str)
1727                        #
1728                        # see http://projects.edgewall.com/trac/changeset/2799
1729                        from trac.ticket.notification import TicketNotifyEmail
1730                        from trac import config as trac_config
1731                elif version == '0.11':
1732                        from trac import attachment
1733                        from trac.env import Environment
1734                        from trac.ticket import Ticket
1735                        from trac.web.href import Href
1736                        from trac import config as trac_config
1737                        from trac import util
1738
1739
1740                        #
1741                        # return  util.text.to_unicode(str)
1742                        #
1743                        # see http://projects.edgewall.com/trac/changeset/2799
1744                        from trac.ticket.notification import TicketNotifyEmail
1745                else:
1746                        print 'TRAC version %s is not supported' %version
1747                        sys.exit(1)
1748                       
1749                if settings.has_key('enable_syslog'):
1750                        if SYSLOG_AVAILABLE:
1751                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
1752
1753
1754                # Must be set before environment is created
1755                #
1756                if settings.has_key('python_egg_cache'):
1757                        python_egg_cache = str(settings['python_egg_cache'])
1758                        os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
1759
1760                env = Environment(settings['project'], create=0)
1761
1762                tktparser = TicketEmailParser(env, settings, float(version))
1763                tktparser.parse(sys.stdin)
1764
1765        # Catch all errors ans log to SYSLOG if we have enabled this
1766        # else stdout
1767        #
1768        except Exception, error:
1769                if ENABLE_SYSLOG:
1770                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
1771
1772                        etype, evalue, etb = sys.exc_info()
1773                        for e in traceback.format_exception(etype, evalue, etb):
1774                                syslog.syslog(e)
1775
1776                        syslog.closelog()
1777                else:
1778                        traceback.print_exc()
1779
1780                if m:
1781                        tktparser.save_email_for_debug(m, True)
1782
1783                sys.exit(1)
1784# EOB
Note: See TracBrowser for help on using the repository browser.