【 SendEmail.py 筆記 - 01 】原始程式碼解析


Posted by bessyhuang on 2023-03-11

心得小記:
我從學術界成功轉職進入業界後,這是我成為雲端系統工程師後直屬主管交付給我的第一項任務。

撰寫目的

透過自動化根據規則「發送通知信」並「停權或移除使用者帳戶」,避免手動操作所帶來的風險。

功能要求

  • 對於「超過一年未登入」的使用者帳戶
    • 發送通知信
  • 對於「超過一年又一個月未登入」的使用者帳戶
    • 移除帳戶:若「上一次登入時間(last_login)」和「帳戶被創建日期(date_joined)」相同
    • 停權帳戶:若「上一次登入時間(last_login)」和「帳戶被創建日期(date_joined)」不同

原始程式碼

import os, sys, time, socket, smtplib
import logging, logging.config

# Database: sqlite
import sqlite3

from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
from email.header import Header

from datetime import datetime
from dateutil.relativedelta import relativedelta
from dateutil import parser


# Email Content
email_subject = 'Notification Email'
body_msg = 'It has been more than a year since you last logged into your account.'
file_path = './account_info.txt'

# Database: sqlite
db_path = './mysite/'
db_name = 'db.sqlite3'

# Time Settings
today = datetime.now()
one_year_ago = today - relativedelta(years=1)
one_year_and_one_month_ago = today - relativedelta(years=1, month=1)

#def get_current_time():
#    now = datetime.now()
#    dt_date = now.strftime("%Y-%m-%d")
#    dt_time = now.strftime("%H:%M:%S")
#    return dt_date, dt_time

def get_account_info():
    con = sqlite3.connect(db_path + db_name)
    cur = con.cursor()
    res = cur.execute("SELECT username, email, date_joined, last_login, is_active FROM users_customuser")
    #print(res.fetchall())
    account_info_list = list(res.fetchall())
    with open(file_path, 'w') as f:
        f.write(str(account_info_list))
    return account_info_list

def send_email_rules(account_info_list):
    acc_email_list = []
    for accinfo in account_info_list:
        acc_username = accinfo[0]
        acc_email = accinfo[1]
        acc_date_joined = parser.parse(accinfo[2])
        acc_last_login = parser.parse(accinfo[3])
        acc_is_active = accinfo[4]
        if acc_last_login < one_year_ago:
            # Over 1 year
            print('Over 1 year not login !', acc_username, acc_last_login, acc_email)
            if acc_email != '':
                acc_email_list.append(acc_email)
            else:
                pass
        else:
            # Under 1 year
            print('Under 1 year:', acc_username, acc_last_login)
    send_email(email_subject, body_msg, acc_email_list, file_path=file_path)

def action_remove_account(username, email, is_active):
    con = sqlite3.connect(db_path + db_name)
    cur = con.cursor()
    cur.execute("DELETE FROM users_customuser WHERE username = '{}' AND email = '{}';".format(username, email))
    con.commit()

def action_inactivate_account(username, email, is_active):
    con = sqlite3.connect(db_path + db_name)
    cur = con.cursor()
    cur.execute("UPDATE users_customuser SET is_active = 0 WHERE username = '{}' AND email = '{}';".format(username, email))
    con.commit()

def manage_account_rules(account_info_list):
    for accinfo in account_info_list:
        acc_username = accinfo[0]
        acc_email = accinfo[1]
        acc_date_joined = parser.parse(accinfo[2])
        acc_last_login = parser.parse(accinfo[3])
        acc_is_active = accinfo[4]
        if acc_last_login < one_year_and_one_month_ago:
            # Over 1 year and 1 month
            print('Over 1 year and 1 month not login !', acc_username, acc_last_login, acc_email)
            if acc_date_joined == acc_last_login:
                print(acc_username, acc_date_joined, acc_last_login, '--------- DEL ACCOUNT')
                action_remove_account(acc_username, acc_email, acc_is_active)
            else:
                print(acc_username, acc_date_joined, acc_last_login, 'INACTIVATE')
                action_inactivate_account(acc_username, acc_email, acc_is_active)
        else:
            # Under 1 year and 1 month
            pass

def send_email(email_subject, body_msg, acc_email_list, file_path=None):
    RECEIVERS = acc_email_list
    #...

if __name__ == '__main__':
    account_info_list = get_account_info()
    send_email_rules(account_info_list)
    manage_account_rules(account_info_list)

改善建議與理由

  1. 請將程式碼「模組化」👉👉👉 超級重要!
    • 將一份程式碼 SendEmail.py,拆分為 SendEmail.pyManageAccount.py,使得 SendEmail.py 可被 ManageAccount.py(主程式碼)引入使用。
  2. 不需要 get_account_info()
    • get_account_info() 直接寫進 manage_account_rules() 即可。
    • 而且不要把 DB 內的資料全抓出來,即使用 find({}),因為當大量 Query Request 的時候會極度耗時、沒效率。
  3. 寄送通知信時,請一封封分開寄,不要用 email list。
    • 因為使用 email list 寄送,表示其他收件者也會看到這封通知信寄給了誰,易有機敏資料洩漏的疑慮。
  4. 請將以下程式碼合併:
    • get_account_info()collection.find({})
    • send_email_rules()Over 1 year 區塊
    • manage_account_rules()Over 1 year and 1 month 區塊(Inactivate/Remove Account)
  5. 執行 DB 的 delete 或 update ... 操作時,必須考慮 collections 之間的關聯性。
    • 關聯性低依賴影響程度低 的必須優先處理,避免 delete 或 update 時出現 Error。
DB 內的 collections,包括 authcustomuser、authcustomuser_group 和 authtoken。 

DB schema 如下:
    authcustomuser
        "_id" : ObjectId()
        "username" : string
        "is_active" : boolean
        "last_login" : ISODate()
        "date_joined" : ISODate()
        "email" : string

    authcustomuser_groups
        "_id" : ObjectId()
        "customuser_id" : ObjectId()
        "group_id" : integer

    authtoken_token
        "_id" : ObjectId()
        "key" : string
        "user_id" : ObjectId()

PS. authcustomuser._id == authcustomuser_groups.customuser_id == authtoken_token.user_id

結論:
    * authcustomuser_group 和 authtoken 必須先刪
    * authcustomuser 必須最後才刪,因為該 collection 被兩個 collections 所依賴。

#Coding #經驗分享







Related Posts

React 入門 0 - 開始寫 React 之前

React 入門 0 - 開始寫 React 之前

找到適合自己的學習方式-隨時想到就寫

找到適合自己的學習方式-隨時想到就寫

當一個 Scrum Master 是一個怎樣的體驗?

當一個 Scrum Master 是一個怎樣的體驗?


Comments