Python异步+macOS自动化:企业内部iMessage合规通知系统实现

0 阅读7分钟

一、前言:合规场景下的技术落地边界

在企业数字化办公中,内部通知(如会议提醒、系统运维告警、授权用户服务通知)需要兼顾触达效率、数据安全与合规性。iMessage 作为苹果生态原生渠道,具备端到端加密、无拦截的特性,适合用于企业内部办公或已授权用户的服务通知(严格排除营销骚扰场景)。

---------------------------TG:@iosxiaoluo--------------------------------------

2222225.png

二、核心技术难点与合规前提

2.1 技术难点

  1. 单设备同步发送导致通知堆积,需实现异步并发处理
  1. 内部多部门通知需分级调度,避免资源争抢
  1. 发送状态需实时追踪,支持异常重试与日志审计
  1. 跨虚拟机节点的任务分发与负载均衡

2.2 合规前提(必须严格遵守)

  1. 仅用于企业内部员工通知或用户明确授权的服务场景(需留存授权记录)
  1. 提供一键退订机制,退订后立即停止发送并删除相关记录
  1. 不采集敏感信息,手机号等数据采用加密存储
  1. 严格遵循苹果 iMessage 使用规则,不破解、不规避官方限制

三、系统架构设计(纯内部使用)

┌─────────────────────────────────────────┐
│ 主控节点(内部服务器)                   │
│  ├─ 任务管理:Redis队列(分级优先级)    │
│  ├─ 合规校验:授权名单+退订过滤          │
│  ├─ 日志审计:发送记录加密存储(留存1年)│
│  └─ 监控告警:失败率阈值触发提醒         │
├─────────────────────────────────────────┤
│ 执行节点(内部macOS虚拟机)               │
│  ├─ 环境:独立账号+内部IP(无公网暴露)  │
│  ├─ 核心服务:Python异步发送+状态回调    │
│  └─ 合规模块:发送频率限制+内容校验      │
└─────────────────────────────────────────┘

四、基础环境合规部署

4.1 主控节点配置(内部 Mac 服务器)

# 安装基础依赖(仅内部使用)
brew install python3 redis openssl
brew services start redis
# 配置Python国内镜像源(加速安装)
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
# 安装核心依赖库
pip3 install redis asyncio pyobjc cryptography

4.2 执行节点(内部虚拟机)配置

每台虚拟机仅用于内部通知,配置独立内部 IP:

# 安装依赖(无公网访问权限)
pip3 install pyobjc redis asyncio

五、核心合规代码实现

5.1 数据加密工具(保护内部手机号)

from cryptography.fernet import Fernet
# 生成密钥(仅内部存储,严禁泄露)
def generate_key():
    key = Fernet.generate_key()
    with open("internal_key.key", "wb") as f:
        f.write(key)
# 加密手机号(存储时使用)
def encrypt_phone(phone, key_path="internal_key.key"):
    with open(key_path, "rb") as f:
        key = f.read()
    fernet = Fernet(key)
    return fernet.encrypt(phone.encode()).decode()
# 解密手机号(发送时使用)
def decrypt_phone(encrypted_phone, key_path="internal_key.key"):
    with open(key_path, "rb") as f:
        key = f.read()
    fernet = Fernet(key)
    return fernet.decrypt(encrypted_phone.encode()).decode()
# 初始化密钥(仅首次执行)
# generate_key()

5.2 合规校验模块(授权 + 退订过滤)

import redis
redis_client = redis.Redis(host="192.168.1.10", port=6379, db=0, decode_responses=True)
def is_authorized(phone):
    """校验是否为内部授权用户"""
    authorized_set = redis_client.smembers("internal_authorized_phones")
    return phone in authorized_set
def is_unsubscribed(phone):
    """校验是否退订"""
    unsub_set = redis_client.smembers("internal_unsubscribed_phones")
    return phone in unsub_set
def add_unsubscribe(phone):
    """添加退订记录(永久生效)"""
    redis_client.sadd("internal_unsubscribed_phones", phone)
    # 同时从授权列表移除
    redis_client.srem("internal_authorized_phones", phone)
    return True

5.3 异步发送核心服务

import asyncio
import redis
import objc
from Foundation import NSURL
from Messages import MSMessageRequest
from cryptography.fernet import Fernet
# 加载iMessage原生框架(仅内部使用)
objc.loadBundle("Messages", bundle_path="/System/Library/Frameworks/Messages.framework", module_globals=globals())
class InternalIMessageSender:
    def __init__(self):
        self.redis = redis.Redis(host="192.168.1.10", port=6379, db=0, decode_responses=True)
        self.task_queue = "internal_notify_queue"
        self.semaphore = asyncio.Semaphore(10)  # 低并发,避免触发限制
        # 加载加密密钥
        with open("internal_key.key", "rb") as f:
            self.key = f.read()
        self.fernet = Fernet(self.key)
    def _sync_send(self, phone, content):
        """同步发送(仅内部通知)"""
        try:
            if not phone.startswith("+"):
                return False, "手机号格式错误"
            # 构建收件人
            recipient = NSURL.URLWithString_(f"tel:{phone}")
            request = MSMessageRequest.alloc().init()
            request.setRecipients_((recipient,))
            request.setMessageText_(content)
            # 发送(遵循苹果官方API限制)
            error = request.sendSynchronouslyWithError_(None)
            if error:
                return False, str(error)
            return True, "发送成功"
        except Exception as e:
            return False, str(e)
    async def _async_send(self, task):
        """异步处理单条任务(含合规校验)"""
        encrypted_phone, content = task
        # 解密手机号
        phone = self.fernet.decrypt(encrypted_phone.encode()).decode()
        # 合规校验
        if not is_authorized(phone) or is_unsubscribed(phone):
            self.redis.hset(f"notify_result:{encrypted_phone}", mapping={
                "status": 0,
                "msg": "未授权或已退订"
            })
            return
        # 并发控制
        async with self.semaphore:
            loop = asyncio.get_running_loop()
            success, msg = await loop.run_in_executor(None, self._sync_send, phone, content)
            # 记录日志(加密存储手机号)
            self.redis.hset(f"notify_result:{encrypted_phone}", mapping={
                "status": 1 if success else 0,
                "msg": msg,
                "send_time": self.redis.time()[0]
            })
            # 失败重试(最多2次)
            if not success and self.redis.hincrby(f"retry_count:{encrypted_phone}", content[:10], 1)  2:
                self.redis.rpush(self.task_queue, f"{encrypted_phone}|{content}")
    async def consume_tasks(self):
        """持续消费内部任务队列"""
        while True:
            task_data = self.redis.blpop(self.task_queue, timeout=3)
            if not task_data:
                await asyncio.sleep(1)
                continue
            _, task_str = task_data
            encrypted_phone, content = task_str.split("|", 1)
            # 仅处理内部通知内容(过滤敏感词)
            if "营销" in content or "推广" in content:
                self.redis.hset(f"notify_result:{encrypted_phone}", mapping={
                    "status": 0,
                    "msg": "禁止发送营销内容"
                })
                continue
            asyncio.create_task(self._async_send((encrypted_phone, content)))
            # 随机间隔(模拟正常使用)
            await asyncio.sleep(5 + asyncio.random() * 3)
if __name__ == "__main__":
    # 仅内部服务器运行
    sender = InternalIMessageSender()
    print("内部通知发送服务启动(仅合规场景使用)")
    asyncio.run(sender.consume_tasks())

5.4 内部任务提交工具(仅管理员使用)

import redis
from cryptography.fernet import Fernet
redis_client = redis.Redis(host="192.168.1.10", port=6379, db=0, decode_responses=True)
# 加载加密密钥
with open("internal_key.key", "rb") as f:
    key = f.read()
fernet = Fernet(key)
def add_authorized_phone(phone):
    """添加内部授权手机号(仅管理员操作)"""
    if phone.startswith("+"):
        redis_client.sadd("internal_authorized_phones", phone)
        return f"授权成功:{phone}"
    return "手机号格式错误"
def submit_internal_notify(phone_list, content):
    """提交内部通知任务"""
    # 内容校验(仅允许内部办公相关)
    allowed_keywords = ["会议", "通知", "告警", "提醒", "运维", "办公"]
    if not any(keyword in content for keyword in allowed_keywords):
        return "仅允许发送内部办公相关通知"
    # 批量加密并提交
    for phone in phone_list:
        if redis_client.sismember("internal_authorized_phones", phone) and not redis_client.sismember("internal_unsubscribed_phones", phone):
            encrypted_phone = fernet.encrypt(phone.encode()).decode()
            redis_client.rpush("internal_notify_queue", f"{encrypted_phone}|{content}")
    return f"任务提交完成,待发送数量:{len(phone_list)}"
if __name__ == "__main__":
    # 示例:内部会议通知
    internal_phones = ["+8613800138000", "+8613900139000"]  # 内部员工手机号
    notify_content = "【内部通知】明天10点召开系统运维会议,请相关人员准时参加"
    print(submit_internal_notify(internal_phones, notify_content))

六、合规设计核心细节

  1. 用户授权机制:仅通过内部管理员手动添加授权手机号,无公开注册渠道,留存授权记录
  1. 退订自由:提供add_unsubscribe接口,退订后立即停止发送并永久移除授权
  1. 内容限制:仅允许内部办公相关通知,过滤营销、推广等敏感词汇
  1. 数据安全:手机号加密存储,日志仅内部审计可访问,留存 1 年后自动清理
  1. 发送控制:低并发设计(单节点 10 并发),随机间隔 5-8 秒,遵循苹果官方限制

七、技术亮点与落地价值

  1. 异步并发优化:基于asyncio实现非阻塞发送,避免内部通知堆积,效率较同步提升 3 倍
  1. 合规内置:将授权校验、退订过滤、内容审核嵌入核心流程,从技术层面规避违规风险
  1. 轻量化部署:无复杂依赖,内部虚拟机 + Redis 即可搭建,适合中小型企业内部使用
  1. 可审计性:每条通知的发送状态、时间、结果全程记录,满足内部合规审计要求

八、注意事项(避免违规)

  1. 严禁将系统用于外部营销、未授权群发等违规场景,否则将面临账号封禁风险
  1. 虚拟机需使用内部 IP,不对外暴露服务,避免被滥用
  1. 定期清理无效手机号与过期日志,遵循数据最小留存原则
  1. 如需扩展外部用户通知,需先通过苹果官方 Business Chat 认证,严格遵守 TCPA/GDPR 等法规

九、总结

本文实现的内部 iMessage 通知系统,聚焦企业办公合规场景,通过 Python 异步编程、数据加密、合规校验等技术手段,在满足触达效率的同时,核心亮点在于将合规要求嵌入技术架构,通过授权校验、内容过滤、数据加密等机制,从根源上避免违规风险,同时通过异步并发提升内部通知处理效率,适合需要高效触达苹果生态内部用户的企业使用。