0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

受信したメールからgoogleカレンダーに予定登録をする仕組みを作った話

Posted at

受信したメールからgoogleカレンダーに予定登録をする仕組みを作った話

社内であるPoCを実施するにあたり検証環境でメールサーバを構築する必要があったため、前からやってみようとしてた社内メールから送った件名と本文をローカルのメールサーバで受信しgoogleカレンダーにイベントを登録する仕組みを作ったので備忘メモとする。
※社内独自のスケジュール管理ツールを使っており、個人のカレンダーなどに登録ができない環境の為、このような事にチャレンジしております(古い会社なんです)

環境

Vmware Esxi7.0
OS:Rocky Linux release 9.3
Dovecont(※バージョンは非公開)
Postfix(※バージョンは非公開)

ネットワークとセキュリティ周り

・ルータのポート開放
・メールサーバの証明書取得(安定のletsencrypt)
・DDNS登録(noipを使ってます)
こちらは、他に参考できるサイトがありますのでそちらをご参照下さい。

コード

※8割型ChatGPTに作ってもらいました。
 コードに関してはド素人なので、ココ間違っているなどあればご指摘下さい。

import imaplib
import email
import quopri
import webbrowser
import os
import logging
import datefinder
import datetime
import re
from email.header import decode_header
from email import quoprimime
from google.oauth2 import service_account
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

# ログの設定
logging.basicConfig(filename='ログの保存先',
                    level=logging.INFO,
                    format='%(asctime)s - %(levelname)s - %(message)s')

# メールサーバーの設定
mail_server = 'SMTPサーバのアドレス'
mail_port = 該当のポート
mail_user = '利用するアカウント'
mail_password = 'パスワード'

# Google Calendar APIの設定
SCOPES = ['https://www.googleapis.com/auth/calendar']
SERVICE_ACCOUNT_FILE = 'jsonファイルの保存先'
CALENDAR_ID = (
    'イベント登録を行いたいカレンダーID'
)

# 予定となるメールの件名に対する正規表現パターン
subject_pattern = re.compile(r'予定', re.IGNORECASE)
cancel_pattern = re.compile(r'キャンセル', re.IGNORECASE)

# IMAPセッションの開始
#mail = imaplib.IMAP4_SSL(mail_server, mail_port)
#mail.login(mail_user, mail_password)

# メールボックスの選択
#mail.select('inbox')

# 未読メールを検索
#status, messages = mail.search(None, '(UNSEEN)')
#if status == 'OK':

    # Google Calendar APIセッションの開始
#    creds = service_account.Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES)
#    service = build('calendar', 'v3', credentials=creds)

#    for num in messages[0].split():
#        process_email(mail, num, service)

def process_email(mail, num, service):
    # メールの取得
    status, msg_data = mail.fetch(num, '(RFC822)')
    if status != 'OK':
        return

    raw_email = msg_data[0][1]
    msg = email.message_from_bytes(raw_email)

    # 件名のデコード
    subject, encoding = decode_header(msg.get("Subject"))[0]
    if isinstance(subject, bytes):
        try:
            subject = subject.decode(encoding or "utf-8")
        except LookupError:
            # もしエンコーディングが不明な場合は iso-2022-jp としてデコード
            subject = subject.decode("iso-2022-jp", errors='replace')
    logging.debug(f"Processing email with subject: {subject}")

    # 件名が予定またはキャンセルとなるものであるか確認
    if subject_pattern.search(subject) or cancel_pattern.search(subject):
        logging.info(f"Found email with subject: {subject}")

        # 本文の取得
        if msg.is_multipart():
            for part in msg.walk():
                if part.get_content_type() in ["text/plain", "text/html"]:
                    body = part.get_payload(decode=True)

                    # エンコーディングと Content-Transfer-Encoding の確認
                    charset = part.get_content_charset()
                    transfer_encoding = part.get('Content-Transfer-Encoding', '').lower()

                    if charset == 'iso-2022-jp' and transfer_encoding == 'quoted-printable':
                        body = quopri.decodestring(body).decode('iso-2022-jp', errors='replace')
                    else:
                        body = body.decode(charset or 'utf-8', errors='replace')
                    logging.debug(f"Email body:\n{body}")

                    # 予定またはキャンセルの処理
                    plan_content_match = re.search(r'予定内容:\s*(.+)', body)
                    if cancel_pattern.search(subject):
                        handle_cancel_event(plan_content_match, service)
                    else:
                        handle_add_event(plan_content_match, body, service)
                    break
        # メールを既読にする
        mail.store(num, '+FLAGS', '\Seen')

def handle_cancel_event(plan_content_match, service):
    if not plan_content_match:
        return

    plan_content = plan_content_match.group(1)

    # Google Calendar APIを使用して予定を削除
    try:
        events = service.events().list(calendarId=CALENDAR_ID).execute().get('items', [])

        for event in events:
            if 'summary' in event and event['summary'] == plan_content:
                service.events().delete(calendarId=CALENDAR_ID, eventId=event['id']).execute()
                logging.info(f"Cancelled event: {plan_content}")

    except HttpError as error:
        logging.error(f'An error occurred while cancelling event: {error}')

def handle_add_event(plan_content_match, body, service):
    if not (plan_content_match and re.search(r'時間:\s*(.+)', body)):
        return

    plan_content = plan_content_match.group(1)
    time_str = re.sub(r'\r', '', re.search(r'時間:\s*(.+)', body).group(1))

    start_time = datetime.datetime.strptime(time_str, '%Y-%m-%d %H:%M')
    end_time = start_time + datetime.timedelta(hours=1)  # 1時間のイベントとして設定

    event = {
        'summary': plan_content,
        'description': f'予定内容: {plan_content}\n時間: {time_str}',
        'start': {'dateTime': start_time.strftime('%Y-%m-%dT%H:%M:%S'), 'timeZone': 'Asia/Tokyo'},
        'end': {'dateTime': end_time.strftime('%Y-%m-%dT%H:%M:%S'), 'timeZone': 'Asia/Tokyo'},
    }

    try:
        logging.debug(f"API Request: {event}")
        created_event = service.events().insert(calendarId=CALENDAR_ID, body=event).execute()
        logging.info(f"Event added: {created_event['htmlLink']}")
    except HttpError as error:
        logging.error(f'An error occurred while adding event: {error}')

# IMAPセッションの開始
mail = imaplib.IMAP4_SSL(mail_server, mail_port)
mail.login(mail_user, mail_password)

# メールボックスの選択
mail.select('inbox')

# 未読メールを検索
status, messages = mail.search(None, '(UNSEEN)')

if status == 'OK':
    # Google Calendar APIセッションの開始
    creds = service_account.Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES)
    service = build('calendar', 'v3', credentials=creds)

    for num in messages[0].split():
        process_email(mail, num, service)

# セッションの終了
mail.logout()

仕組みを作って気づいたこと

社内メールサーバのContent-Typeとエンコーディングを確認せずに作っていたこともありgmailから送ったものは処理されるが、社内メールから送ったものは処理されないなど色々ハマってしまいました。
元のソースがどういうものなのか、確認した上でコードを作ると改めて勉強になりました。

参考

https://www.rem-system.com/mail-postfix03/
https://www.rem-system.com/mail-postfix01/
https://centossrv.com/postfix.shtml

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?