|
# -*- coding: utf-8 -*- |
|
""" |
|
References |
|
---------- |
|
- [pywin32 - Sync all folders outlook win32 Python - Stack Overflow](https://stackoverflow.com/a/69789358) |
|
""" |
|
import sys |
|
sys.coinit_flags = 0 # 0: Multi-Threaded Apartment model (MTA), 2: Single-Threaded Apartment model (STA) |
|
# [備忘] pythoncomの機能をマルチスレッドで利用しようとする場合、pythoncomをimportする前にsys.coinit_flags = 0を設定しておく必要あり |
|
import pythoncom |
|
import win32com.client |
|
import win32api |
|
import win32con |
|
import threading |
|
import signal |
|
import logging |
|
from enum import Enum |
|
from types import SimpleNamespace |
|
|
|
|
|
KeyboadInterruptOperation = Enum('KeyboadInterruptOperation', [ |
|
'NotSet', |
|
'Ignore', |
|
'StopSync', |
|
], start=0) |
|
|
|
|
|
class SyncResult(SimpleNamespace): |
|
def __init__(self, |
|
sync_object_name = '(unknown)' |
|
): |
|
self.sync_object_name = sync_object_name |
|
self.is_complete = False |
|
self.is_timeout = False |
|
self.is_interrupt = False |
|
self.error_code = 0 |
|
self.error_description = '' |
|
self.interrupt = None |
|
|
|
def __dir__(self): |
|
return ['sync_object_name', 'is_complete', 'is_timeout', 'is_interrupt', 'error_code', 'error_description', 'interrupt',] |
|
|
|
|
|
def get_sync_thread( |
|
sync_object, |
|
timeout = None, |
|
start_individually = True |
|
): |
|
class SyncHandler(object): |
|
@property |
|
def sync_result(self): |
|
return self.__sync_result |
|
|
|
def __init__(self, *args, **kwargs): |
|
super().__init__(*args, **kwargs) |
|
self.__logger = logging.getLogger(f'SyncHandler') |
|
self.__stop_requested = False |
|
self.__sync_result = SyncResult() |
|
|
|
def init(self, parameters): |
|
self.__parameters = parameters |
|
self.__sync_result.sync_object_name = sync_object_name = parameters.sync_object.Name |
|
self.__parent_thread_id = parent_thread_id = parameters.parent_thread_id if hasattr(parameters, 'parent_thread_id') else win32api.GetCurrentThreadId() |
|
self.__logger = logging.getLogger(f'SyncHandler({sync_object_name})[{parent_thread_id}]') |
|
|
|
def OnSyncStart(self): |
|
self.__debug_print('OnSyncStart') |
|
|
|
def OnProgress(self, _State_, _Description_, _Value_, _Max_): |
|
self.__debug_print(f'OnProgress: {_State_}.{_Description_} ({100 * _Value_ / _Max_}%)') |
|
|
|
def OnSyncEnd(self): |
|
self.__debug_print(f'OnSyncEnd') |
|
if self.__sync_result.error_code == 0: |
|
self.__sync_result.is_complete = True |
|
self.__stop_request() |
|
|
|
def OnError(self, _Code_, _Description_): |
|
self.__debug_print(f'OnError: {_Code_}.{_Description_}') |
|
self.__sync_result.error_code = _Code_ |
|
self.__sync_result.error_description = _Description_ |
|
self.__stop_request() |
|
|
|
def __stop_request(self): |
|
if not self.__stop_requested: |
|
self.__post_quit() |
|
self.__stop_requested = True |
|
|
|
def __post_quit(self): |
|
#parent_thread_id = win32api.GetCurrentThreadId() # [備忘] この時点で取得したスレッドIDは期待したものにならないことに注意 |
|
parent_thread_id = self.__parent_thread_id |
|
win32api.PostThreadMessage(parent_thread_id, win32con.WM_QUIT, 0, 0) # WM_QUIT送信によりSyncThread中のPumpMessages()を中断 |
|
|
|
def __debug_print(self, *args, **kwargs): |
|
self.__logger.debug(*args, **kwargs) |
|
|
|
|
|
class SyncThread(threading.Thread): |
|
@property |
|
def sync_result(self): |
|
return self.__sync_result |
|
|
|
def __init__(self, *args, **kwargs): |
|
super().__init__(*args, **kwargs) |
|
self.__sync_object = sync_object = self._kwargs['sync_object'] |
|
self.__logger = logging.getLogger(f'SyncThread({sync_object.Name})') |
|
self.__timeout = self._kwargs.get('timeout') |
|
self.__start_individually = self._kwargs.get('start_individually', True) |
|
self.__is_timeout = False |
|
self.__timer = None |
|
self.__is_interrupt = False |
|
self.__stop_requested = False |
|
self.__sync_result = SyncResult(sync_object_name=sync_object.Name) |
|
|
|
def run(self): |
|
sync_object = self.__sync_object |
|
self.__logger = logging.getLogger(f'SyncThread({sync_object.Name})[{self.ident}]') |
|
timeout = self.__timeout |
|
if timeout is not None: |
|
def on_timeout(): |
|
self.__debug_print(f'Timeout') |
|
self.__is_timeout = True |
|
self.__timer = None |
|
self.__stop_request() |
|
|
|
self.__timer = timer = threading.Timer(timeout, on_timeout) |
|
timer.start() |
|
|
|
pythoncom.CoInitializeEx(pythoncom.COINIT_MULTITHREADED) |
|
|
|
sync_handler = win32com.client.WithEvents(sync_object, SyncHandler) |
|
sync_handler.init(SimpleNamespace( |
|
sync_object = sync_object, |
|
parent_thread_id = self.ident, |
|
)) |
|
|
|
if self.__start_individually: |
|
sync_object.Start() |
|
|
|
pythoncom.PumpMessages() # WM_QUITを受信するまでブロック |
|
sync_handler.close() |
|
|
|
sync_result = sync_handler.sync_result |
|
if self.__is_timeout: |
|
sync_result.is_timeout = True |
|
else: |
|
if self.__timer is not None: |
|
self.__timer.cancel() |
|
self.__timer = None |
|
sync_result.is_interrupt = self.__is_interrupt |
|
self.__sync_result = sync_result |
|
|
|
pythoncom.CoUninitialize() |
|
|
|
def stop_request(self): |
|
self.__debug_print('stop_request() called') |
|
self.__is_interrupt = True |
|
self.__stop_request() |
|
|
|
def __stop_request(self): |
|
if not self.__stop_requested: |
|
self.__sync_object.Stop() |
|
self.__stop_requested = True |
|
|
|
def __debug_print(self, *args, **kwargs): |
|
self.__logger.debug(*args, **kwargs) |
|
|
|
return SyncThread(kwargs=dict(sync_object=sync_object, timeout=timeout, start_individually=start_individually)) |
|
|
|
|
|
def trigger_send_and_receive_all( |
|
outlook_application = None |
|
): |
|
logger = logging.getLogger(f'trigger_send_and_receive_all()') |
|
|
|
if outlook_application is None: |
|
outlook_application = win32com.client.Dispatch('Outlook.Application') |
|
|
|
namespace = outlook_application.GetNamespace('MAPI') |
|
namespace.SendAndReceive(showProgressDialog = False) |
|
|
|
logger.debug('done') |
|
|
|
|
|
def sync_all( |
|
timeout = 600, |
|
outlook_application = None, |
|
keyboard_interrupt_operation = None, |
|
target_syncobject_name_list = None, |
|
use_trigger_send_and_receive_all = False |
|
): |
|
logger = logging.getLogger(f'sync_all()') |
|
|
|
if outlook_application is None: |
|
outlook_application = win32com.client.Dispatch('Outlook.Application') |
|
|
|
if keyboard_interrupt_operation is None: |
|
keyboard_interrupt_operation = KeyboadInterruptOperation.NotSet |
|
|
|
try: |
|
target_syncobject_name_list = list(target_syncobject_name_list) |
|
except Exception as error: |
|
target_syncobject_name_list = [] |
|
|
|
target_specified = (0 < len(target_syncobject_name_list)) |
|
if target_specified: |
|
use_trigger_send_and_receive_all = False |
|
|
|
interrupt = None |
|
if keyboard_interrupt_operation != KeyboadInterruptOperation.NotSet: |
|
def signal_handler(signum, frame): |
|
nonlocal interrupt |
|
interrupt = KeyboardInterrupt() |
|
|
|
signal.signal(signal.SIGINT, signal_handler) |
|
|
|
namespace = outlook_application.GetNamespace('MAPI') |
|
sync_info_list = [] |
|
for sync_object in namespace.SyncObjects: |
|
if target_specified and (not sync_object.Name in target_syncobject_name_list): |
|
continue |
|
sync_thread = get_sync_thread(sync_object, timeout=timeout, start_individually=(not use_trigger_send_and_receive_all)) |
|
sync_info_list.append(SimpleNamespace( |
|
sync_object = sync_object, |
|
sync_thread = sync_thread, |
|
)) |
|
sync_thread.start() |
|
|
|
if len(sync_info_list) < 1: |
|
return [] |
|
|
|
if use_trigger_send_and_receive_all: |
|
trigger_send_and_receive_all(outlook_application) |
|
|
|
sync_result_list = [] |
|
for sync_info in sync_info_list: |
|
sync_thread = sync_info.sync_thread |
|
|
|
''' |
|
#sync_thread.join() # [備忘] 単にjoin()とするとキーボード割込(Ctrl+C)が効かない |
|
''' |
|
|
|
''' |
|
#while sync_thread.is_alive(): |
|
# sync_thread.join(timeout=0.1) # [備忘] join()でtimeoutを設定することでキーボード割込(Ctrl+C)が効くようになる |
|
''' |
|
|
|
''' |
|
#while sync_thread.is_alive(): |
|
# if interrupt is not None: |
|
# sync_thread.stop_request() |
|
# sync_thread.join() |
|
# break |
|
# |
|
# sync_thread.join(timeout=0.1) |
|
# try: |
|
# sync_thread.join(timeout=1) |
|
# except KeyboardInterrupt as error: |
|
# # [備忘] |
|
# # なぜかここにきた段階で該当スレッドが終了してしまっている(sync_thread.is_alive()がFalseになる) |
|
# # →やむを得ず、interruptの書き換えはsignal.signal(signal.SIGINT, signal_handler)で登録したsignal_handler()内で実施 |
|
# interrupt = error |
|
''' |
|
|
|
while sync_thread.is_alive(): |
|
if (keyboard_interrupt_operation == KeyboadInterruptOperation.StopSync) and (interrupt is not None): |
|
sync_thread.stop_request() |
|
sync_thread.join() |
|
break |
|
|
|
sync_thread.join(timeout=0.1) |
|
|
|
sync_result = sync_thread.sync_result |
|
sync_result.interrupt = interrupt |
|
logger.debug(sync_result) |
|
sync_result_list.append(sync_result) |
|
|
|
return sync_result_list |
|
|
|
|
|
if __name__ == '__main__': |
|
# [Test] |
|
log_level = logging.DEBUG # logging.(NOTSET|DEBUG|INFO|WARNING|ERROR|CRITICAL) |
|
logging.basicConfig(level=log_level, format='{asctime} [{levelname}] {name}: {message}', style='{') |
|
for timeout in (None, 1.0,): |
|
logging.info(f'(*) timeout={timeout}') |
|
sync_result_list = sync_all(timeout=timeout) |
|
#sync_result_list = sync_all(timeout=timeout, keyboard_interrupt_operation=KeyboadInterruptOperation.StopSync) |
|
#sync_result_list = sync_all(timeout=timeout, use_trigger_send_and_receive_all=True) |
|
#sync_result_list = sync_all(timeout=timeout, target_syncobject_name_list=['新しい送受信グループ #1',]) |
|
#sync_result_list = sync_all(timeout=timeout, target_syncobject_name_list=['新しい送受信グループ #1',], use_trigger_send_and_receive_all=True,) |
|
for sync_result in sync_result_list: |
|
logging.info('-'*80) |
|
for name in dir(sync_result): |
|
value = getattr(sync_result, name) |
|
if (value is not None) and (not (type(value) in (bool, int, float, str,))): |
|
value = '<object>' |
|
logging.info(f'{name}: {value}') |
|
logging.info('='*80) |