Home › Help › How email works (sending and spam avoidance)
How email works (sending and spam avoidance)
ShootCal sends two very different kinds of email, and it treats them differently on purpose.
Anything your client sees comes from YOU. Booking confirmations, your typed replies to an inquiry, a follow-up note: all of these go out through your own connected Gmail account. The From line is literally your real email address, so to the client it looks exactly like you wrote it by hand from Gmail. A reply from them lands back in your Gmail, and a copy sits in your Gmail Sent folder. ShootCal is not a middleman mailbox here. It just asks Gmail to send the message as you.
The one email that does NOT come from you is the heads-up to yourself when a new booking request arrives. That notification comes from a dedicated ShootCal address. There is a good reason: Gmail does not reliably handle an email that is sent from your own address back to your own address with a special reply target on it. By sending the alert from a separate ShootCal address, ShootCal can attach a private, per-request reply address so that when you just hit Reply in Gmail, your answer is captured, forwarded to the client as you, and the request is automatically marked as handled in the app.
Nothing is faked or spoofed. Your client mail is genuinely from your Gmail (Google signs it). The notification is genuinely from a dedicated domain ShootCal controls. The whole design avoids the one thing that gets mail flagged: ShootCal never tries to send from an address it is not allowed to send from.
The two-sender design at a glance
ShootCal deliberately splits outgoing mail across two senders depending on who the recipient is.
Client-facing mail (sent FROM the photographer's own Gmail via the Gmail API's gmail.send scope):
- The session confirmation email
- The 'send test email' button (it sends to yourself to prove sending works)
- A typed reply to a booking inquiry
- A relayed reply that you wrote directly in Gmail
- The optional client booking confirmation when Notify client is on
Photographer notification (sent FROM a dedicated ShootCal address via Brevo):
- The 'new booking request' alert to you, but only when reply-tracking is configured.
Every one of the client-facing messages is built as a multipart email and sent through the Gmail API, whose From header is the photographer's real address. The notification is built as a plain payload and sent through Brevo from the dedicated ShootCal address.
How client mail is actually sent (the Gmail API)
ShootCal encodes the full email message and posts it to the Gmail API's send endpoint using your OAuth access token. Because Google sends it as the signed-in user, the From is your genuine Gmail address, Google applies its own DKIM signature, and a copy appears in your Gmail Sent folder. Your token is resolved fresh for each request: the stored token is decrypted, and refreshed from your encrypted refresh token when it has expired. If Gmail rejects the send because the connection has lapsed, the client-facing actions surface a 'reconnect required' message rather than failing silently.
The message body is a standard multipart email with both a plain-text part and an HTML part, so every mail program can render it. Display names are encoded carefully: non-ASCII characters are escaped, and a name containing a comma is quoted so Gmail does not misread it as two addresses. The Gmail connection uses the narrow gmail.send scope, which can send mail as you but cannot read your mailbox.
The booking-request notification (Brevo + per-request Reply-To)
The alert that lands in your own inbox leads with a client-facing summary (session, when, client name, email, phone, the client's full message) and then, below a divider line reading '----- Below this line is just for you, not sent to the client -----', it includes your private accept/decline magic link and the app inbox URL.
When Brevo and a tracking domain are configured, the notification is sent through Brevo from the dedicated ShootCal address with its Reply-To set to a private, per-request tracking address (the request's own secret token, at the tracking domain). Sending from a DIFFERENT address than the recipient is what lets Gmail honor that single Reply-To: Gmail mishandles a Reply-To on an email you send to yourself. The send is forced over IPv4 because the Brevo connection is locked to the server's IPv4 address.
If Brevo is NOT configured, the notification falls back to the original behavior: a self-email through your own Gmail with From 'ShootCal <your-address>' and Reply-To set to the CLIENT's email, so hitting Reply mails the client directly. The instant-book ('confirmed') variant always uses this Gmail path and carries no tracking address.
The reply relay: Gmail or in-app reply, back out to the client as you
There are two ways a photographer answers an inquiry, and both end with a message that leaves from the photographer's own Gmail.
Path A, reply in the app: ShootCal takes your typed body (markdown-lite: bold, [label](url), bare links), renders both a plain-text and an HTML part, and sends it through the Gmail API from your address with Reply-To set to yourself, so the rest of the conversation continues in your Gmail. The request is marked as answered.
Path B, reply directly in Gmail: you just hit Reply on the notification. Because the notification's Reply-To was the private per-request tracking address, the reply is routed by Cloudflare Email Routing to a Cloudflare email worker, which forwards the raw message (carrying the request's token) to ShootCal over an authenticated, secret-guarded channel. That step is not tied to your app session: it is trusted only because it presents the shared secret. ShootCal looks up the request by its token (matched case-insensitively), verifies the sender's From equals the request owner's email (only your own reply counts), extracts just your freshly typed reply, then relays it to the client through the Gmail API from your Gmail and marks the request as answered. Crucially, the request is only marked answered when the relay actually succeeded (or there was genuinely nothing to relay), so a failed send leaves it pending in the inbox.
Why the private accept/decline link can never leak to the client
The notification carries an internal accept/decline magic link, so the relay must forward ONLY your freshly typed text. ShootCal does this in layers.
First it pulls the plain-text part out of the (possibly multipart) message, or converts the HTML part to text. Then it keeps only the lines above the quoted history: it breaks at the legacy 'Reply above this line' marker, at Gmail/Apple Mail 'On ... wrote:' attributions (including the wrapped 'On ... <email@' variant), at Outlook's '----- Original Message -----', at our own 'Below this line is just for you' divider, at quoted notification headers ('request through your ShootCal', 'confirmed time through your ShootCal'), and at any line that contains the magic-link path.
Then a wrap-tolerant final guard runs: it accumulates each line's whitespace-stripped, lowercased text and hard-cuts as soon as the magic-link path OR this request's own secret token (8+ characters) appears in the running join, defeating the case where an email client wraps the long URL across lines. The relayed reply is also length-capped. Net effect: the internal link and token cannot reach the client even through HTML collapse or line wrapping.
Deliverability: authenticated sending, no spoofing
Two independent trust stories, neither of which spoofs an address.
Client mail: it is genuinely sent through Gmail as you (the gmail.send scope), so Google applies your own DKIM signature and the mail aligns with the Gmail/Google Workspace domain it actually came from. To the receiving server it is an ordinary, authenticated Gmail message from you, which is why it lands in the inbox and replies thread naturally in the client's mail app.
Notification mail: it is sent from a dedicated sending domain ShootCal controls, through Brevo's transactional API. For a sending domain like this the standard deliverability setup is the usual authentication records (an SPF record authorizing the sending infrastructure, DKIM signing, and a DMARC policy); those records live in DNS, not in the application code, so the app code itself does not assert their state. Cloudflare Email Routing on that domain receives the replies to the per-request tracking addresses and hands them to the email worker.
The key point for spam avoidance is in the design, not the DNS: ShootCal never sends from an address it is not authorized to send from. Client mail comes from your real Gmail (authenticated by Google); notifications come from ShootCal's own Brevo-sent domain. There is no spoofing of the client's domain or of your domain by a third party, which is the pattern that gets flagged as spam or fails DMARC.
Where the data lives (concretely)
Each booking request is stored server-side, scoped to your account: the client's chosen slot, their name, email, and phone, any note, the request's status, and the secret token that doubles as the per-request reply address. Your sending address is your connected Gmail address, and your business or display name comes from your booking-page settings. The reply preference (answer in Gmail vs answer only in the app) is part of your saved settings; in app-only mode no notification email is sent at all and you answer in the app.
The two-sender behavior is driven by a small set of configuration values: the Brevo credentials, the dedicated tracking domain and notification address, the shared secret the Cloudflare email worker presents, and the base URLs for the accept/decline magic links and the requests inbox. These are server configuration, not anything a photographer sets.
FAQ
When a client gets a confirmation or a reply, what does the From address actually say?
Your real Gmail address, with your business or personal name as the display name. The message is sent through Google's Gmail API as you, using your OAuth token, so it is a genuine email from your account, it is DKIM-signed by Google, and a copy lands in your Gmail Sent folder. The client can reply and it threads straight back to your inbox.
Why is the new-request notification sent from a ShootCal address instead of from my own Gmail?
So that a single per-request Reply-To can be honored. Gmail mishandles a Reply-To on a self-addressed email (an email from you, to you, with a different reply target). By sending the alert from a separate ShootCal address via Brevo, ShootCal can attach a private per-request reply address, so when you hit Reply your answer is captured and relayed. If Brevo is not configured, ShootCal falls back to a self-email from your Gmail with Reply-To set to the client instead.
If I just reply in Gmail, how does my answer reach the client without me CC-ing them?
Your reply goes to the private per-request tracking address (it was the Reply-To on the notification). Cloudflare Email Routing forwards it to a Cloudflare email worker, which hands the raw message to ShootCal over an authenticated, secret-guarded channel. ShootCal verifies the secret, confirms the sender is you (the From must equal the request owner's email), strips out everything except your freshly typed text, and re-sends that to the client from your own Gmail. The client only ever sees a message from your address.
Could the private accept/decline link in the notification ever get forwarded to the client?
No, there are three guards. First, ShootCal keeps only the lines above the quoted notification (breaking at attribution lines, the 'just for you' divider, quoted notification headers, and any line containing the magic-link path). Second, a wrap-tolerant final cut accumulates lowercased, whitespace-stripped text and hard-cuts the moment the magic-link path or this request's own secret token appears, defeating clients that wrap the long URL across lines. Third, the magic link itself lives below an explicit '----- Below this line is just for you, not sent to the client -----' divider in the notification body.
What stops these emails from landing in spam?
Authenticated sending with no spoofing. Client mail is sent as you through Gmail, so it carries Google's DKIM and aligns with your actual sending domain. The notification is sent from a dedicated domain ShootCal controls, through Brevo. The thing the design guarantees is that ShootCal never sends from an address it is not authorized for, so nothing fails domain alignment, which is the usual cause of spam-foldering or DMARC rejection. The standard SPF, DKIM, and DMARC records for the notification domain are part of its DNS setup rather than something the app enforces in code.
What is the per-request tracking address, exactly?
It is the request's own secret token used as the address at ShootCal's dedicated tracking domain. The same token is the secret in your accept/decline magic link. The inbound step matches it case-insensitively, because an email address can be case-normalized in transit, and it only accepts a token of the expected length and character set.
If the relay fails, does the request still get marked as answered?
No. A request is marked answered only when the relay to the client actually succeeded (or when the stripped reply text was genuinely empty so there was nothing to send). If the Gmail send fails (for example a lapsed Gmail connection, a send error, or an invalid client email), the request stays pending so your inbox still shows it as unanswered, rather than misleading you into thinking the client received a reply. The same care applies to in-app replies, which check the send result instead of assuming success.
Are these emails plain text or HTML?
Both. ShootCal builds a multipart message with a plain-text part and an HTML part, and every client-facing email path uses it, including the in-app one-off reply. The HTML is rendered from a 'markdown-lite' subset: bold, [label](url) links (the url may be http(s) or mailto), bare http(s) links that get autolinked, and line breaks. It escapes all user content first and only then converts the small known set of markers, so nothing a photographer or a quoted client note types can inject markup. Note bare mailto: addresses are only linked when written in the explicit [label](mailto:...) form, not autolinked on their own. The plain-text fallback is derived the same way (the templated confirmation and test emails pass plain text and let ShootCal derive the HTML).
What scopes and tokens are involved, and what happens if mine expires?
Sending uses your Google OAuth access token, resolved fresh for each request (your stored token is decrypted and refreshed from your encrypted refresh token when needed) and used against the Gmail send endpoint. The Gmail connection is the narrow gmail.send scope, which can send as you but cannot read your mailbox. If Google rejects the send because the connection has lapsed, the client-facing actions return a 'reconnect required' message asking you to sign out and back in. The inbound relay is separate: it is not tied to your app session and is authenticated by the shared secret the Cloudflare email worker presents, so a stranger cannot post to it, and replies from anyone other than the request owner are ignored.
Does ShootCal store or read my client emails as a mailbox?
It is not an inbox, and it could not be one even if it wanted to: the Gmail connection is send-only, which cannot read mail. The app fires messages and records status. The only inbound processing is the relay: the Cloudflare email worker passes the raw reply to ShootCal, which extracts just your typed text to forward and then discards the rest. The stored request keeps the booking facts and status; your actual email conversation lives in your Gmail, where the relayed messages thread.