Postfixでgmail宛の制限

# Postfixでgmail宛の制限

gmail宛に１秒間隔、同時接続数3を実現したい。
smtp-destination_concurrency_limit
smtp-destination_rate_delay
は同時に設定できない(同時に設定しても同時接続数1になる)ので、
無理矢理実現。

以下は頑張って読もう
https://www.postfix-jp.info/trans-2.1/jhtml/access.5.html

```bash
mkdir -p /etc/postfix/userprogram/bin/
mkdir -p /etc/postfix/userprogram/def/
mkdir -p /etc/postfix/userprogram/log/
chown -R nobody:nobody /etc/postfix/userprogram/
```

```/etc/postfix/userprogram/bin/check_policy_service.py
import sys
import logging
import socket
import os
import re
import yaml
from pathlib import Path
import random

# テスト用
import io
if len(sys.argv) > 1:
    sys.stdin = io.StringIO(sys.argv[1])

#===============================================================================
# ロギング設定
#===============================================================================
def setup_logging():
    host_name = socket.gethostname()
    program_name = os.path.basename(__file__)
    log_name = Path(__file__).stem + '.log'
    log_file_path = Path(__file__).resolve().parent.parent / 'log' / log_name
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    file_handler = logging.FileHandler(log_file_path, encoding='utf-8')
    file_handler.setFormatter(logging.Formatter(f'%(asctime)s {host_name} [{program_name}:%(levelname)s] [%(process)d] %(message)s', datefmt='%Y-%m-%d %H:%M:%S'))
    stream_handler = logging.StreamHandler(sys.stdout)
    stream_handler.setFormatter(logging.Formatter(f'%(asctime)s {host_name} [{program_name}:%(levelname)s] [%(process)d] %(message)s', datefmt='%Y-%m-%d %H:%M:%S'))
    logger.addHandler(file_handler)
    logger.addHandler(stream_handler)

#===============================================================================
# ユーティリティ関数
#===============================================================================
def load_transport_config(config_path):
    """
    設定ファイルの読み込み。YAML構造が変更されたため、適応。
    """
    with open(config_path, 'r') as file:
        config = yaml.safe_load(file)  # YAMLファイルの読み込み
    services_config = config['check_policy_services']  # 新しいトップレベルキーに対応
    compiled_services = []
    for service_item in services_config:
        for service_name, settings in service_item.items():
            compiled_domains = [re.compile(domain) for domain in settings['domains']]
            compiled_services.append({
                'name': service_name,
                'domains': compiled_domains,
                'range': settings['range']
            })
    return compiled_services

def match_domain(transport_config, recipient_domain):
    """
    ドメインの一致を判定する。
    """
    for service in transport_config:
        for domain_pattern in service['domains']:
            if domain_pattern.match(recipient_domain):
                return service['name'], service['range']
    return None, None

#================================================================================
# メイン関数
#================================================================================
def main():
    # ロギング設定の初期化
    setup_logging()

    # 設定ファイルのパス
    config_path = Path(__file__).resolve().parent.parent / 'def' / (Path(__file__).stem + '.yaml')
    transport_config = load_transport_config(config_path)

    try:
        for line in sys.stdin:
            line = line.strip()
            if line == "":
                logging.info("Empty line detected, stopping.")
                break
            if line.startswith("recipient="):
                recipient = line.split("recipient=")[-1]
                if "@" not in recipient:
                    print("action=DUNNO\n")
                    logging.error(f"{recipient}: action=DUNNO, Invalid email address")
                    continue
                recipient_domain = '@' + recipient.split("@")[-1]
                service, rng = match_domain(transport_config, recipient_domain)
                if service:
                    selected_number = random.randint(1, rng)
                    print(f"action=FILTER {service}{selected_number}:\n")
                    logging.info(f"{recipient}: action=FILTER {service}{selected_number}:")
                else:
                    print("action=DUNNO\n")
                    logging.info(f"{recipient}: action=DUNNO")
                sys.stdout.flush()
                break

    except Exception as e:
        logging.error(f"Error processing request: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()
```

```/etc/postfix/userprogram/def/check_policy_service.yaml
check_policy_services:
  - docomo-smtp:
      domains:
        - '@docomo\.ne\.jp$'
        - '@disneymobile\.ne\.jp$'
      range: 4
  - au-smtp:
      domains:
        - '@ezweb\.ne\.jp$'
        - '@.+\.ezweb\.ne\.jp$'
      range: 3
  - softbank-smtp:
      domains:
        - '@softbank\.ne\.jp$'
        - '@[dhtckrnsq]\.vodafone\.ne\.jp$'
      range: 3
  - gmail-smtp:
      domains:
        - "@gmail.com$"
      range: 3
```

```/etc/postfix/master.cf
policy_service unix - n n - - spawn
    user=nobody argv=/etc/postfix/userprogram/bin/check_policy_service.py
docomo-smtp1 unix - - - - - smtp
docomo-smtp2 unix - - - - - smtp
docomo-smtp3 unix - - - - - smtp
docomo-smtp4 unix - - - - - smtp
au-smtp1 unix - - - - - smtp
au-smtp2 unix - - - - - smtp
au-smtp3 unix - - - - - smtp
softbank-smtp1 unix - - - - - smtp
softbank-smtp2 unix - - - - - smtp
softbank-smtp3 unix - - - - - smtp
gmail-smtp1 unix - - - - - smtp
gmail-smtp2 unix - - - - - smtp
gmail-smtp3 unix - - - - - smtp
````

```/etc/postfix/main.cf
smtpd_recipient_restrictions = check_policy_service unix:private/policy_service
docomo-smtp1_destination_rate_delay=1s
docomo-smtp2_destination_rate_delay=1s
docomo-smtp3_destination_rate_delay=1s
docomo-smtp4_destination_rate_delay=1s
au-smtp1_destination_rate_delay=1s
au-smtp2_destination_rate_delay=1s
au-smtp3_destination_rate_delay=1s
softbank-smtp1_destination_rate_delay=1s
softbank-smtp2_destination_rate_delay=1s
softbank-smtp3_destination_rate_delay=1s
gmail-smtp1_destination_rate_delay=1s
gmail-smtp2_destination_rate_delay=1s
gmail-smtp3_destination_rate_delay=1s
```