Style improvements and cache from settings validation added
[imap-fix-internaldate] / src / fix_imap_internaldate.py
CommitLineData
c9da760a
PD
1'''
2fix_imap_internaldate.py - Fix the INTERNALDATE field on IMAP servers
3
4Copyright (c) 2012 Intra2net AG
5Author: Plamen Dimitrov
6
7This program is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or
10(at your option) any later version.
11
12This program is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
c9da760a
PD
16'''
17
d0cfa9d0 18import sys
c9da760a 19import csv
d0cfa9d0
PD
20import argparse
21# python version handling
22try:
23 import configparser
24except ImportError:
25 print("This module needs python version 3 or later.")
26 sys.exit()
8fe4e3ff 27import logging
8a9d4c89 28from mail_date_parser import MailDateParser
c9da760a 29from mail_iterator import MailIterator
8301e589 30from caching_data import CachingData
c9da760a 31
6177b21d
TJ
32CONFIG_FILENAME = "fix_imap_internaldate.cfg"
33LOG_FILENAME = "fix_imap_internaldate.log"
34CSV_FILENAME = "userdata.csv"
35
c9da760a 36def main():
97bd6bea
PD
37 """Interprets command arguments and initializes configuration and logger.
38 Then begins mail synchronization."""
648f0037 39
97bd6bea 40 # parse arguments
648f0037
PD
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')
c9da760a 47
8fe4e3ff 48 # config and logging setup
c9da760a 49 config = load_configuration()
8fe4e3ff 50 prepare_logger(config)
97bd6bea 51 args = parser.parse_args()
648f0037 52 if(args.test_mode):
f9fd9b29 53 logging.info("Testing mode initiated. No message will be modified on the server.")
648f0037 54 else:
f9fd9b29 55 logging.info("Update mode initiated. Messages will be modified.")
3b81023f 56
97bd6bea 57 # proceed to main functionality
d0cfa9d0
PD
58 try:
59 synchronize_csv(config, args.test_mode)
60 except KeyboardInterrupt:
61 logging.info("Script was interrupted by the user.")
97bd6bea 62
28d8aa17 63 logging.info("All done. Exiting.")
97bd6bea
PD
64 return
65
66def load_configuration():
67 """Loads the script configuration from a file or creates such."""
68 config = configparser.RawConfigParser()
6177b21d 69 success = config.read(CONFIG_FILENAME)
95467f63 70 try:
db3f09a6
PD
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.getboolean('basic_settings', 'skip_shared_folders')
76 config.getboolean('basic_settings', 'fallback_to_date_header')
89241463 77 except configparser.NoSectionError:
95467f63 78 success = []
db3f09a6
PD
79 except configparser.NoOptionError:
80 success = []
95467f63
PD
81 except ValueError:
82 success = []
83
84 # if corrupted settings save file and load default
97bd6bea 85 if(len(success)==0):
95467f63
PD
86 if(not config.has_section('basic_settings')):
87 config.add_section('basic_settings')
97bd6bea
PD
88 config.set('basic_settings', 'file_log_level', logging.INFO)
89 config.set('basic_settings', 'console_log_level', logging.INFO)
90 config.set('basic_settings', 'imap_server', 'imap.company.com')
a936a06b 91 config.set('basic_settings', 'tolerance_mins', 30)
a05fef0a
PD
92 config.set('basic_settings', 'skip_shared_folders', True)
93 config.set('basic_settings', 'fallback_to_date_header', False)
6177b21d 94 with open(CONFIG_FILENAME, 'w') as configfile:
97bd6bea 95 config.write(configfile)
ef29ccfb
TJ
96 configfile.write("# 0 NOTSET, 10 DEBUG, 20 INFO, 30 WARNING, 40 ERROR, 50 CRITICAL\n")
97
97bd6bea
PD
98 return config
99
100def prepare_logger(config):
101 """Sets up the logging functionality"""
6177b21d 102
97bd6bea 103 # reset the log
6177b21d 104 with open(LOG_FILENAME, 'w'):
97bd6bea 105 pass
6177b21d 106
97bd6bea 107 # add basic configuration
6177b21d 108 logging.basicConfig(filename=LOG_FILENAME,
97bd6bea
PD
109 format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
110 level=config.getint('basic_settings', 'file_log_level'))
6177b21d 111
97bd6bea
PD
112 # add a handler for a console output
113 console = logging.StreamHandler()
114 console.setLevel(config.getint('basic_settings', 'console_log_level'))
115 formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
116 console.setFormatter(formatter)
117 logging.getLogger('').addHandler(console)
118 return
119
120def synchronize_csv(config, test_mode):
121 """Iterates through csv list of users and synchronizes their messages."""
122
123 # initialize loop permanent data
db3f09a6 124 caching_data = CachingData(config.getboolean('basic_settings', 'fallback_to_date_header'))
97bd6bea 125 date_parser = MailDateParser()
c9da760a 126 server = config.get('basic_settings', 'imap_server')
a936a06b 127 tolerance = config.getint('basic_settings', 'tolerance_mins') * 60
c9da760a 128
97bd6bea 129 # iterate through the users in the csv data
6177b21d 130 user_reader = csv.DictReader(open(CSV_FILENAME, "r"), delimiter=',')
c9da760a
PD
131 for user in user_reader:
132 try:
95467f63 133 session = MailIterator(server, user['username'], user['password'],
a05fef0a 134 config.getboolean('basic_settings', 'skip_shared_folders'))
c9da760a
PD
135 except UserWarning as ex:
136 logging.error(ex)
137 continue
138 for mailbox in session:
139 try:
7a1d4c35 140 box = caching_data.retrieve_cached_mailbox(mailbox[0], mailbox[1], user['username'])
c9da760a 141 mail_ids = session.fetch_messages()
3b81023f 142 new_ids = box.synchronize(mail_ids, tolerance)
6f2bc406 143 logging.info("%s new messages out of %s found in %s.", len(new_ids), len(mail_ids), box.name)
c9da760a
PD
144 except UserWarning as ex:
145 logging.error(ex)
146 continue
8301e589 147 for mid in new_ids:
c9da760a
PD
148 try:
149 fetched_internal_date = session.fetch_internal_date(mid)
8a9d4c89 150 internal_date = date_parser.extract_internal_date(fetched_internal_date)
87cde111
PD
151 fetched_correct_date = session.fetch_received_date(mid)
152 correct_date = date_parser.extract_received_date(fetched_correct_date)
153 # check for empty received headers
154 if(correct_date == ""):
d5ccfbec 155 logging.debug("No received date could be found in message uid: %s - mailbox: %s - user: %s.",
8a9d4c89 156 mid.decode('iso-8859-1'), box.name, box.owner)
7a1d4c35 157 box.no_received_field += 1
87cde111 158 # correct these messages if required and override received_date from basic date
a05fef0a 159 if(config.getboolean('basic_settings', 'fallback_to_date_header')):
87cde111
PD
160 fetched_correct_date = session.fetch_basic_date(mid)
161 correct_date = date_parser.extract_received_date(fetched_correct_date)
031ddbdf
TJ
162 if(correct_date == ""):
163 logging.debug("No fallback date header could be found in message uid: %s - mailbox: %s - user: %s.",
164 mid.decode('iso-8859-1'), box.name, box.owner)
165
166 if(correct_date == ""):
87cde111
PD
167 # skip synchronization for this message
168 continue
169 else:
170 # preserve only the first received line as fetched if everything is ok
171 fetched_correct_date = fetched_correct_date.split("Received:")[1]
c9da760a
PD
172 except UserWarning as ex:
173 logging.error(ex)
174 continue
87cde111
PD
175 if(date_parser.compare_dates(correct_date, internal_date, tolerance)):
176 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.",
36f4d196
TJ
177 mid.decode('iso-8859-1'), box.name, box.owner,
178 internal_date.strftime("%d %b %Y %H:%M:%S"),
87cde111
PD
179 correct_date.strftime("%d %b %Y %H:%M:%S"),
180 fetched_correct_date)
5a4bc5d8 181 if(not test_mode):
c9da760a 182 try:
87cde111 183 session.update_message(mid, box.name, correct_date)
c9da760a
PD
184 except UserWarning as ex:
185 logging.error(ex)
186 continue
36f4d196 187
8301e589 188 # count total emails for every user and mailbox
7a1d4c35 189 box.date_conflicts += 1
36f4d196 190
8301e589 191 # if all messages were successfully fixed confirm caching
97bd6bea 192 if(not test_mode):
7a1d4c35 193 box.confirm_change()
6177b21d 194
08ba5736
PD
195 # final report on date conflicts
196 caching_data.report_conflicts()
8fe4e3ff
PD
197 return
198
c9da760a
PD
199if(__name__ == "__main__"):
200 main()