Email to Web App Actions
Order emails become actions in a web app that has no API.
Problem
Orders arrive by email with a PDF notice, and each must quickly become a job in a third-party web app. The sender cannot change the format, and the app has no API.
Approach
The integration sends the same HTTP requests as the app's own pages, under the account's login. The mailbox is the queue, and an unread email is an order not yet submitted. The email is marked read right after the job is created, so a retry never submits an order twice.
Solution
flowchart LR
EM[order email, unread, with a PDF] --> EX[read the PDF's own text]
EX --> VAL{fields valid?}
VAL -->|no| ISS[stop, open an issue]
VAL -->|yes| CK[session, cached up to 48 h]
CK --> DP[create the call, as the page would]
DP --> MR[mark the email read]
MR --> CF[confirm to the sender]
CF --> LG[log a row, once per call number]
LG --> TR[watch the call]for mail in unread_orders():
order = validate(read_notice(mail.pdf)) # raise before anything irreversible
call = dispatch(order)
mark_read(mail) # commit point: retries after this never re-dispatch
confirm(mail.sender, call); log(call); track(call)I created, searched and deleted a job in the browser and copied each request as a curl command. Replaying them from a shell showed the required headers and fields. A discovery script created a test job for each dropdown code, read it back through the search endpoint, and deleted it. The read-back found 22 values where the captured traffic showed 6.
Notice fields come from the PDF's text layer and are validated before any request.
sequenceDiagram
participant F as Flow
participant V as Web app
participant S as Secret store
F->>S: session younger than 48 h?
alt no
F->>V: get the login page
V-->>F: form and anti-forgery token
F->>V: post credentials and token
V-->>F: session cookie
F->>S: keep the cookie
end
F->>V: create the call, as the page's own request
V-->>F: call idThe login loads the form, copies its hidden state and anti-forgery token, and posts them with the credentials. The session cookie is reused for up to 48 hours, the account owner's limit.
def session():
c = secrets.get("app/session")
if c and age(c) < hours(48): # account owner's limit
return c
page = http.get(LOGIN)
token = find_input(page, "anti-forgery-token")
r = http.post(LOGIN, data={"user": U, "pass": P, "token": token})
return secrets.put("app/session", r.cookies["session"])
def dispatch(order):
return http.post(CALLS, json=order, # bare: a wrapped body made an empty call
cookies=session(),
headers={"x-requested-with": "XMLHttpRequest"})A body wrapped in an extra JSON object returned success but created an empty job, so the body is sent bare. A tracker polls each job every 5 minutes and sends a final email when the job is complete.
flowchart LR
T[(calls being watched)] --> M[every 5 minutes, look at each]
M --> C{can it still be completed?}
C -->|yes| T
C -->|no| E[final email, team copied] --> D[stop watching]
M -->|error| TOutcome
| Measure | Value |
|---|---|
| Double dispatches found, then closed | 36 of 295 |
| Earlier dispatches with the wrong ID | 294 |
| Retry time per notice in a reader outage | 46 s |
| Longest time between logins | 48 h |
| Checks for new orders, for completion | every 1 min, every 5 min |
Two overlapping runs had read the same unread email before either marked it read. One run at a time removed the duplicates. Turnaround was not measured.
Applicability
The approach fits any web app without an API, such as scheduling tools, property management systems and supplier portals.