App feedback → Feishu SMTP
For SafeLine owning public ports 80/443 and OpenResty in host mode on 10080/12443, follow the Chinese SafeLine deployment guide and openresty-safeline-http.conf.example. TLS terminates at SafeLine; its origin is OpenResty's HTTP port 10080.
Prepared for Ubuntu + 1Panel on 43.138.57.216. Public endpoint: https://app.glowtps.cn/v1/feedback. Server sender: authors@glowtps.cn; fixed receiver: tps-david@glowtps.cn. This directory contains deployment source; no connection to the server or live SMTP test has been performed.
Contract
POST /v1/feedback uses multipart/form-data only. Each text field occurs once; repeated attachments use the field name attachments.
| Field | Problem feedback | Development request |
|---|---|---|
submission_id |
Required random identifier, 16–128 characters [A-Za-z0-9_-] (UUID v4 works). Keep unchanged for retries. |
Same |
type |
problem |
development |
message |
Required, nonblank, max 5000 Unicode characters | Must be omitted, even if empty |
contact |
Optional, max 200 characters | Required, nonblank, max 200 characters |
school |
Must be omitted | Required, nonblank, max 200 characters |
attachments |
Optional, max 3 files, 8 MiB each, 20 MiB aggregate | Must be omitted |
diagnostics |
Optional single UTF-8 text file, max 256 KiB, independent of media count; starts with TPSchedule diagnostic log followed by a newline |
Must be omitted |
diagnostic_html |
Optional single UTF-8 inert HTML file for undergraduate or graduate imports, max 2 MiB, independent of media count; starts with <!doctype html><!-- TPSchedule school diagnostic --> |
Must be omitted |
The app defaults diagnostics to off. When selected it snapshots the preceding ten minutes of structured app/login/import/reminder events, at most 500 entries. It excludes credentials, cookies, raw URLs, HTML, timetable contents and feedback text. The server attaches this file as TPSchedule-diagnostics.txt; retries must use the identical snapshot and submission ID. The server hashes it with the other attachments for idempotency, and does not log its contents.
To enable this field on an existing deployment, replace the service source and rebuild the same Compose service, keeping .env, the idempotency secret and database volume. Existing clients without diagnostics remain compatible. Do not alter the SafeLine/OpenResty routing for this update. See 日志附件更新说明.
Accepted MIME types: image/jpeg, image/png, image/gif, image/webp, image/heic, image/heif, video/mp4, video/quicktime, video/x-m4v, video/webm. Send an explicit matching MIME type; application/octet-stream is rejected. Container signatures must match. MOV/MP4/M4V/HEIC require an initial valid ftyp box with a recognized compatible brand; legacy MOV without ftyp is rejected. Checks identify file formats; they do not certify an attachment is harmless or a complete playable file.
Filenames lose directory components, control characters and unsupported characters, and are bounded. Nothing is served as public media. All text goes into a plain-text mail body; sender/recipient and mail headers never come from submitted text.
Success is HTTP 200 with exactly:
{"success":true,"submission_id":"client-generated-id","status":"sent"}
This means the configured SMTP server acknowledged message acceptance with 250, and that status was committed locally. It does not guarantee inbox delivery or reading. Same-ID/same-content retries return the recorded success without sending again. Fingerprints include exact field values, sanitized filenames, MIME types and attachment content in the original order. Retry with the same payload and ID; generate a new ID when editing a previously submitted payload.
All failures use non-2xx responses, with success:false, error, detail and (where applicable) submission_id/status:
| HTTP | Error/status | Action |
|---|---|---|
| 400 / 408 | Malformed body / upload timeout | Fix request or retry the same ID |
| 413 / 415 / 422 | Upload limits / unsupported media / invalid fields | Fix payload; no mail was attempted |
| 409 | id_conflict |
This ID already belongs to other content; do not blindly resend |
| 409 | in_progress |
Wait, then retry the identical ID and payload |
| 429 | rate_limited |
Respect Retry-After |
| 503 | server_busy |
Respect Retry-After; retain the ID |
| 503 | smtp_unavailable, status retryable |
SMTP definitely did not accept mail; retry the same ID |
| 503 | delivery_uncertain or storage_unavailable, status uncertain |
Stop automatic resubmission and ask the operator to check the mail by submission ID |
Proxy-generated errors can be HTML rather than this JSON; clients must still treat them as failures. No 202 or queue acceptance counts as success.
Ubuntu / 1Panel deployment
- Point the DNS A record for
app.glowtps.cnto43.138.57.216. In Tencent Cloud's security group allow web ports 80/443; keep 18080 closed to the public. Use the existing 1Panel/OpenResty installation. - Upload this directory's deployable files, without
.deps, test caches, local database files or any developer environment file, to a dedicated server directory such as/opt/app-feedback. In 1Panel's file manager or server terminal, copy.env.exampleto.env; set its permissions to600. - In Feishu Mail, enable third-party mail-client access for
authors@glowtps.cnand generate the client authorization password. Put it only inSMTP_PASSWORDon the server. UseSMTP_USERNAME=authors@glowtps.cn,MAIL_FROM=authors@glowtps.cnandMAIL_TO=tps-david@glowtps.cn. Single-quote secret values in.envso$is literal in Compose. Do not paste passwords into chat, Flutter code, Dockerfiles, terminal command history or screenshots. - Generate
IDEMPOTENCY_SECRETonce on the server (for exampleopenssl rand -hex 32), insert it into.envand keep it stable across updates. Changing this key while retaining the database makes existing fingerprints conflict. Replace both placeholder secrets before starting. - From the uploaded directory run
docker compose up -d --build(or create a 1Panel Compose project fromdocker-compose.ymlwith this directory as its build context). The image runs as an unprivileged user, the filesystem is read-only except the named SQLite volume and temporary upload space, and the host publishes only127.0.0.1:18080. - Verify liveness locally with
curl --fail http://127.0.0.1:18080/healthzand checkdocker compose ps. Health checks do not send mail or validate SMTP credentials. If startup fails, inspect container logs; the app validates required configuration without printing values. Do not sharedocker compose configordocker inspectoutput, because they can expose credentials. - In 1Panel, create/configure the
app.glowtps.cnreverse-proxy website, obtain/select its HTTPS certificate and enable HTTPS redirection. Configure the upstream ashttp://127.0.0.1:18080. Add the location settings fromreverse-proxy.conf.exampleinside the site's generated HTTPS server block, preserving its certificate paths. If the site already exists, add only the feedback and health locations rather than replacing other routes. In the generated config, avoid duplicatelocation = /v1/feedbackblocks. Test OpenResty's configuration through 1Panel before reload. - Check that 1Panel's OpenResty has host network access. A proxy container on a bridge network has its own
127.0.0.1; it cannot reach a localhost-only host publish. With that setup, configure a shared private Docker network and proxy tofeedback:8000, remove the host port publish, and restrict proxy trust to that network instead of exposing 18080 publicly. The supplied example targets the host-access configuration; inspect the existing network mode before use. - Verify
https://app.glowtps.cn/healthz. When explicitly ready to send a real message, submit one controlled feedback, confirm the response and receipt attps-david@glowtps.cn, then retry the same ID and payload to verify only one mail arrives. That real-mail check remains to be done by the operator.
Feishu's official documentation specifies smtp.feishu.cn with SSL on 465, and its business-system guide documents STARTTLS on 587. The service supports only verified TLS: SMTP_PORT=465 + SMTP_TLS=ssl (default), or SMTP_PORT=587 + SMTP_TLS=starttls. It never falls back to plaintext or disables certificate checks. Outbound 465/587 access must be permitted by the server/network.
Durable retries and retention
SQLite in the named feedback-data volume stores only submission ID, a keyed content fingerprint, state and timestamps. It stores no form text, contact address, school, filename or raw attachment. Uploaded data is spooled to bounded temporary files and deleted on request completion; in Docker, the temporary area is a tmpfs. The resulting email and attachments are retained by Feishu/recipient mailbox policies, independently of this database.
There is no atomic transaction across SMTP and SQLite. A lost DATA acknowledgement or crash after acceptance may mean mail arrived even though the HTTP request failed. Such records are quarantined as uncertain, preventing automatic resend. A sending record stays in_progress for five minutes; afterwards it becomes uncertain. The stable email Message-ID includes the submission ID, but recipient deduplication is not assumed.
Metadata expires after seven days since the last state change (RETENTION_DAYS). Cleanup runs at startup, on submissions, and hourly while the process runs. The retry deduplication guarantee is bounded by this retention window; reusing an ID after expiry can send again. Client automatic retries must stop within the retention window. Keep the volume and key across deployments, and apply an appropriate retention policy to any backups. Use docker compose down for a stop; do not use down -v when preserving retry records. Back up SQLite consistently while the service is stopped or using SQLite's backup API, including its WAL requirements.
To resolve an uncertain record, first check the recipient mailbox and provider delivery records for its submission ID. During a maintenance stop, use a trusted SQLite tool to inspect submissions. Change only that record's status to sent after verified acceptance, or to retryable only after verified non-acceptance, updating updated to the current Unix timestamp. Never automatically clear uncertain records, delete the volume or issue a new submission ID to force a retry.
Limits and operations
The service counts raw HTTP bytes, including chunked input, with a 20 MiB + 256 KiB + 2 MiB + 64 KiB request ceiling; the proxy allows 24 MiB to leave the final precise check to the service. Text parts, field count and file count are bounded. Four active feedback requests are allowed by default, with a 60-second upload deadline. Default limits are ten requests/IP/hour and 100 requests globally/hour. These include invalid requests and retries. The in-memory rate counters reset after restart; SQLite deduplication survives. Proxy IP headers are overwritten; the private upstream must stay private.
The school HTML attachment is a separate explicit opt-in with a privacy dialog, not part of structured logs. Only the latest undergraduate import evidence is retained in memory for ten minutes and cleared on logout. Its known course statements are inert, attributes and unrelated scripts/forms are removed, and it carries a restrictive CSP. Failed delivery retries freeze the same bytes and submission ID. See 0.7.5 HTML附件更新 for existing 1Panel/SafeLine deployments.
Run exactly one worker and one service replica with this configuration: rate/concurrency counters are per-process. SQLite claims serialize concurrent IDs, but scaling needs a shared rate limiter and an appropriate storage design. The default 512 MiB memory and 256 MiB tmpfs budget account for multipart and MIME encoding of several uploads; if reducing these, also reduce concurrency. Large attachments can grow by roughly a third in email MIME encoding and may be rejected by provider message-size limits; a definite rejection returns a failure.
The app logs neither user fields nor SMTP responses/secrets; Uvicorn access logs are disabled. Feedback-path proxy access logs are also disabled in the example. Do not enable SMTP debug logging, request-body/WAF capture or form-content tracing on this endpoint. Allowlisted media are attached to plain-text mail only and never executed by the service. For higher abuse exposure, add server-side account verification/CAPTCHA through the app's existing identity system; no static secret embedded in an APK provides meaningful access control.
Local verification (no real SMTP)
Requires Python 3.11+; the supplied image uses 3.12. Install dependencies into a virtual environment in this directory:
python3 -m venv .venv
. .venv/bin/activate
python -m pip install -r requirements-dev.txt
python -m pytest -q
Tests inject a recording sender or scripted SMTP protocol object. They cover type-specific validation, file MIME/signatures, size/count and chunked-body limits, definite/uncertain SMTP outcomes, verified SSL/STARTTLS order, recipient fixation, persistence/restarts, conflicting IDs, concurrent retries, storage-commit failure, rate/concurrency limits, and metadata privacy. No SMTP connection, credentials or live mail is needed.