目录

通过 Github Actions 推送 The Economist 到个人邮箱

2026年5月8日
6 分钟阅读

感谢 awesome-english-ebooks 项目作者,感谢 Claude Code 老师。

获取邮箱密授权码

我用 QQ 邮箱,需要使用授权码代替邮箱密码。

  1. 登录 QQ 邮箱 → 设置 → 账号与安全
  2. 下滑找到 “POP3/IMAP/SMTP/Exchange/CardDAV 服务”,开启
  3. 按提示发短信验证,获得授权码,保存好

创建 Github 仓库并添加密钥

  1. 在 GitHub 新建一个仓库(随意命名,如 ebook-sync)
  2. 进入仓库 → Settings → Secrets and variables → Actions → New repository secret

添加两项:

  • QQ_EMAIL 你的 QQ 邮箱地址
  • QQ_AUTH_CODE 第一步获取的授权码

设置 Github Actions

在本地 git 初始化一个空文件夹,然后添加 .github/workflows/send-epub.yml 文件,内容如下。完成后推送到 ebook-sync 仓库。

name: Send New EPUBs to Email
on:
schedule:
- cron: '0 22 * * 5' # 每周六 06:00 北京时间
workflow_dispatch:
permissions:
contents: write
jobs:
send-epub:
runs-on: ubuntu-latest
steps:
- name: Checkout magz repo
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Find and download new epub files
id: find_epubs
run: |
# 读取上次处理的 commit SHA
LAST_SHA=""
if [ -f .last_commit ]; then
LAST_SHA=$(cat .last_commit)
fi
# 获取源仓库最新的 commit SHA
LATEST_SHA=$(curl -s \
"https://api.github.com/repos/hehonghui/awesome-english-ebooks/commits?per_page=1" \
| jq -r '.[0].sha')
# 没有新 commit,直接结束
if [ "$LAST_SHA" = "$LATEST_SHA" ]; then
echo "没有新的 commit,无需处理"
echo "has_files=false" >> $GITHUB_OUTPUT
exit 0
fi
# 获取新增的 commits
if [ -z "$LAST_SHA" ]; then
# 首次运行:取最近 7 天
SINCE=$(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || \
date -u -v-7d +%Y-%m-%dT%H:%M:%SZ)
COMMITS=$(curl -s \
"https://api.github.com/repos/hehonghui/awesome-english-ebooks/commits?since=${SINCE}&per_page=100" \
| jq -r '.[].sha')
else
# 后续运行:取 LAST_SHA 之后的所有 commits
COMMITS=$(curl -s \
"https://api.github.com/repos/hehonghui/awesome-english-ebooks/compare/${LAST_SHA}...${LATEST_SHA}" \
| jq -r '.commits[].sha')
fi
# 保存最新 SHA,供后续步骤提交
echo "$LATEST_SHA" > .last_commit
mkdir -p downloads
HAS_ANY=false
for SHA in $COMMITS; do
FILES_JSON=$(curl -s \
"https://api.github.com/repos/hehonghui/awesome-english-ebooks/commits/${SHA}" \
| jq -r '.files[] | select(.status == "added") | select(.filename | ascii_downcase | endswith(".epub")) | [.filename, .raw_url] | @tsv')
while IFS=$'\t' read -r FILENAME RAW_URL; do
[ -z "$FILENAME" ] && continue
DIR=$(echo "$FILENAME" | cut -d'/' -f1)
BASENAME=$(basename "$FILENAME")
SAVE_PATH="downloads/${DIR}___${BASENAME}"
[ -f "$SAVE_PATH" ] && continue
echo "下载: $BASENAME"
curl -sL "$RAW_URL" -o "$SAVE_PATH"
HAS_ANY=true
done <<< "$FILES_JSON"
done
if [ "$HAS_ANY" = true ]; then
echo "has_files=true" >> $GITHUB_OUTPUT
else
echo "has_files=false" >> $GITHUB_OUTPUT
fi
- name: Send one email per magazine
if: steps.find_epubs.outputs.has_files == 'true'
env:
QQ_EMAIL: ${{ secrets.QQ_EMAIL }}
QQ_AUTH_CODE: ${{ secrets.QQ_AUTH_CODE }}
run: |
python3 << 'EOF'
import smtplib
import os
import glob
import zipfile
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
from collections import defaultdict
MAG_NAMES = {
"01_economist": "The Economist",
"02_new_yorker": "The New Yorker",
"04_atlantic": "The Atlantic",
"05_wired": "Wired",
}
SIZE_LIMIT = 50 * 1024 * 1024 # 50MB
email = os.environ["QQ_EMAIL"]
password = os.environ["QQ_AUTH_CODE"]
magazines = defaultdict(list)
for filepath in glob.glob("downloads/*___*.epub"):
filename = os.path.basename(filepath)
dir_key, basename = filename.split("___", 1)
magazines[dir_key].append((basename, filepath))
for dir_key, files in magazines.items():
mag_name = MAG_NAMES.get(dir_key, dir_key)
msg = MIMEMultipart()
msg["Subject"] = f"本周新增 EPUB - {mag_name}"
msg["From"] = email
msg["To"] = email
body_lines = [f"你好,\n\n本周《{mag_name}》新增了以下 EPUB 文件:\n"]
oversized = []
for basename, filepath in files:
size = os.path.getsize(filepath)
if size <= SIZE_LIMIT:
zip_name = basename.replace(".epub", ".zip")
zip_path = filepath.replace(".epub", ".zip")
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
zf.write(filepath, basename)
with open(zip_path, "rb") as f:
part = MIMEBase("application", "octet-stream")
part.set_payload(f.read())
encoders.encode_base64(part)
part.add_header(
"Content-Disposition",
f'attachment; filename="{zip_name}"'
)
msg.attach(part)
body_lines.append(f"- {basename}(附件,zip 格式,解压后即可阅读)")
else:
size_mb = size / 1024 / 1024
url = f"https://github.com/hehonghui/awesome-english-ebooks/raw/master/{dir_key}/{basename}"
oversized.append(f"- {basename}({size_mb:.1f}MB,超过 50MB)\n 下载链接:{url}")
if oversized:
body_lines.append("\n以下文件超过 50MB,以链接形式发送:\n")
body_lines.extend(oversized)
body_lines.append("\n仓库地址:https://github.com/hehonghui/awesome-english-ebooks")
msg.attach(MIMEText("\n".join(body_lines), "plain", "utf-8"))
with smtplib.SMTP_SSL("smtp.qq.com", 465) as server:
server.login(email, password)
server.send_message(msg)
print(f"已发送:{mag_name}")
EOF
- name: Save last processed commit SHA
run: |
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config user.name "github-actions[bot]"
git add .last_commit
git diff --staged --quiet || git commit -m "chore: update last commit SHA [skip ci]"
git push
- name: No new epub this week
if: steps.find_epubs.outputs.has_files == 'false'
run: echo "没有新增 epub 文件,无需发送邮件"

验证

在该仓库的 Actions 页的左侧点击 Send New EPUBs to Email,然后点击右侧的 Run workflow。

测试运行正常的话你会收到四封邮件,内容分别为原杂志仓库各杂志的最新一期提醒,杂志内容打包成 zip 文件1以附件形式附在其中。

后续每周六早晨六点会自动检查新文件并发送。

脚注

  1. QQ 邮箱会对附件内容做审查,发送 Epub 文件会被拦截,打包成 zip 文件可行。