2 fix_imap_internaldate.py - Fix the INTERNALDATE field on IMAP servers
4 Copyright (c) 2012 Intra2net AG
5 Author: Plamen Dimitrov
7 This program is free software: you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation, either version 3 of the License, or
10 (at your option) any later version.
12 This program is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
21 # python version handling
25 print("This module needs python version 3 or later.")
28 from mail_date_parser import MailDateParser
29 from mail_iterator import MailIterator
30 from caching_data import CachingData
32 CONFIG_FILENAME = "fix_imap_internaldate.cfg"
33 LOG_FILENAME = "fix_imap_internaldate.log"
34 CSV_FILENAME = "userdata.csv"
37 """Interprets command arguments and initializes configuration and logger.
38 Then begins mail synchronization."""
41 parser = argparse.ArgumentParser(description="Fix the INTERNALDATE field on IMAP servers. "
42 "Small tool to fix the IMAP internaldate "
43 "in case it's too much off compared to the last date "
44 "stored in the received lines.")
45 parser.add_argument('-u', '--update', dest='test_mode', action='store_false',
46 default=True, help='update all e-mails and exit test mode')
48 # config and logging setup
49 config = load_configuration()
50 prepare_logger(config)
51 args = parser.parse_args()
53 logging.info("Testing mode initiated. No message will be modified on the server.")
55 logging.info("Update mode initiated. Messages will be modified.")
57 # proceed to main functionality
59 synchronize_csv(config, args.test_mode)
60 except KeyboardInterrupt:
61 logging.info("Script was interrupted by the user.")
63 logging.info("All done. Exiting.")
66 def load_configuration():
67 """Loads the script configuration from a file or creates such."""
68 config = configparser.RawConfigParser()
69 success = config.read(CONFIG_FILENAME)
71 config.get('basic_settings', 'file_log_level')
72 config.get('basic_settings', 'console_log_level')
73 config.get('basic_settings', 'imap_server')
74 config.getint('basic_settings', 'tolerance_mins')
75 config.get('basic_settings', 'skip_shared_folders')
76 config.get('basic_settings', 'fallback_to_date_header')
77 except configparser.NoOptionError:
82 # if corrupted settings save file and load default
84 if(not config.has_section('basic_settings')):
85 config.add_section('basic_settings')
86 config.set('basic_settings', 'file_log_level', logging.INFO)
87 config.set('basic_settings', 'console_log_level', logging.INFO)
88 config.set('basic_settings', 'imap_server', 'imap.company.com')
89 config.set('basic_settings', 'tolerance_mins', 30)
90 config.set('basic_settings', 'skip_shared_folders', "ON")
91 config.set('basic_settings', 'fallback_to_date_header', "OFF")
92 with open(CONFIG_FILENAME, 'w') as configfile:
93 config.write(configfile)
94 configfile.write("# 0 NOTSET, 10 DEBUG, 20 INFO, 30 WARNING, 40 ERROR, 50 CRITICAL")
98 def prepare_logger(config):
99 """Sets up the logging functionality"""
102 with open(LOG_FILENAME, 'w'):
105 # add basic configuration
106 logging.basicConfig(filename=LOG_FILENAME,
107 format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
108 level=config.getint('basic_settings', 'file_log_level'))
110 # add a handler for a console output
111 console = logging.StreamHandler()
112 console.setLevel(config.getint('basic_settings', 'console_log_level'))
113 formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
114 console.setFormatter(formatter)
115 logging.getLogger('').addHandler(console)
118 def synchronize_csv(config, test_mode):
119 """Iterates through csv list of users and synchronizes their messages."""
121 # initialize loop permanent data
122 caching_data = CachingData()
123 date_parser = MailDateParser()
124 server = config.get('basic_settings', 'imap_server')
125 tolerance = config.getint('basic_settings', 'tolerance_mins') * 60
127 # iterate through the users in the csv data
128 user_reader = csv.DictReader(open(CSV_FILENAME, "r"), delimiter=',')
129 for user in user_reader:
131 session = MailIterator(server, user['username'], user['password'],
132 config.get('basic_settings', 'skip_shared_folders')=="ON")
133 except UserWarning as ex:
136 for mailbox in session:
138 box = caching_data.retrieve_cached_mailbox(mailbox[0], mailbox[1], user['username'])
139 mail_ids = session.fetch_messages()
140 new_ids = box.synchronize(mail_ids, tolerance)
141 logging.info("%s new messages out of %s found in %s.", len(new_ids), len(mail_ids), box.name)
142 except UserWarning as ex:
147 fetched_internal_date = session.fetch_internal_date(mid)
148 internal_date = date_parser.extract_internal_date(fetched_internal_date)
149 fetched_correct_date = session.fetch_received_date(mid)
150 correct_date = date_parser.extract_received_date(fetched_correct_date)
151 # check for empty received headers
152 if(correct_date == ""):
153 logging.debug("No received date could be found in message uid: %s - mailbox: %s - user: %s.",
154 mid.decode('iso-8859-1'), box.name, box.owner)
155 box.no_received_field += 1
156 # correct these messages if required and override received_date from basic date
157 if(config.get('basic_settings', 'fallback_to_date_header') == "ON"):
158 fetched_correct_date = session.fetch_basic_date(mid)
159 correct_date = date_parser.extract_received_date(fetched_correct_date)
161 # skip synchronization for this message
164 # preserve only the first received line as fetched if everything is ok
165 fetched_correct_date = fetched_correct_date.split("Received:")[1]
166 except UserWarning as ex:
169 if(date_parser.compare_dates(correct_date, internal_date, tolerance)):
170 logging.warning("Date conflict found in message uid: %s - mailbox: %s - user: %s.\nInternal date %s is different from extracted date %s from header:\n%s.",
171 mid.decode('iso-8859-1'), box.name, box.owner,
172 internal_date.strftime("%d %b %Y %H:%M:%S"),
173 correct_date.strftime("%d %b %Y %H:%M:%S"),
174 fetched_correct_date)
177 session.update_message(mid, box.name, correct_date)
178 except UserWarning as ex:
182 # count total emails for every user and mailbox
183 box.date_conflicts += 1
185 # if all messages were successfully fixed confirm caching
189 # final report on date conflicts
190 caching_data.report_conflicts()
193 if(__name__ == "__main__"):