Editor Search Emails

This vignette describes how roreviewapi sends editor-search emails through the Gmail API, sending “from” the [email protected] account and authorized from that same account. Set that account once as an environment variable, and every later step refers back to it:

export USER_EMAIL=[email protected]

This vignette complements the admin setup vignette, covering only the Gmail-specific pieces of the editor volunteer search workflow.

Prerequisites

  • A Google Cloud Console project owned by the rOpenSci Workspace’s Cloud Identity org
  • A local machine with a browser, for the one-time OAuth authorization
  • SSH access to the Digital Ocean droplet
  • 1Password access to [email protected]

1. Google Cloud Console

In the ro-auto-email project:

  1. Enable the Gmail API.
  2. Configure the OAuth consent screen with Audience: Internal (requires the project to be owned by the Workspace’s Cloud Identity org). Internal governs who can authorize the app, not who receives mail — it skips Google verification and the 7-day refresh-token expiry that applies to unverified External apps. Request scope https://www.googleapis.com/auth/gmail.send.
  3. Create an OAuth 2.0 Client ID, type Desktop app. Download client_secret_<id>.apps.googleusercontent.com.json.

2. Authorize once, locally

Run this on a local machine, not the droplet (it needs a browser and a localhost redirect):

library (gmailr)
gm_auth_configure (path = "client_secret_<id>.apps.googleusercontent.com.json")
gm_auth (
    email  = Sys.getenv ("USER_EMAIL"),
    scopes = c ("gmail.modify", "gmail.settings_basic"),
    cache  = "gmailr_token"
)

Log in as $USER_EMAIL and grant consent. This caches a refresh token to the gmailr_token/ directory, which is portable to the droplet afterward since it is tied to the Google account, not the machine.

Always pass the same scopes used here in any later gm_auth() call — a mismatch forces a fresh interactive re-authorization, which does not work headless on the droplet.

3. Code

R/editor-search.R:

  • gmail_auth() configures and authorizes gmailr from the cached credentials.
  • gmail_send(to, subject, html_body) builds and sends one message via gm_mime()/gm_from()/gm_to()/gm_subject()/gm_html_body()/ gm_send_message().
  • gmail_send_batch(emails, links, subject, repo, issue_id) loops gmail_send() once per recipient with Sys.sleep(0.1) between sends, since there is no Gmail equivalent of a batch-send endpoint for personalized messages.

The send_search and handle_click endpoints each take an injectable sender parameter (default gmail_send_batch / gmail_send) for testing.

4. Environment variables

Variable Value
GMAIL_SENDER [email protected]
GMAIL_OAUTH_CLIENT_SECRET /data/email/client_secret_<id>.apps.googleusercontent.com.json
GMAIL_TOKEN_CACHE /data/email/gmailr_token
GMAIL_AUTH_EMAIL $USER_EMAIL

On the droplet, set these in the untracked .env file, as described in the admin setup vignette; use a local .env file for local runs. The two file paths are inside /data/email/, the container side of the bind-mounted data directory, so the files themselves go in /srv/roreviewapi/data/ on the droplet (Section 5).

5. Deploy credentials to the droplet

Copy client_secret_<id>.apps.googleusercontent.com.json and the gmailr_token/ directory to /srv/roreviewapi/data/ (bind-mounted into the container at /data/email). That directory is Docker/root-owned, so scp to your home directory first, then move into place.

Locally:

scp client_secret_<id>.apps.googleusercontent.com.json youruser@droplet:~/
scp -r gmailr_token youruser@droplet:~/

On the droplet:

sudo mv ~/client_secret_<id>.apps.googleusercontent.com.json /srv/roreviewapi/data/
sudo mv ~/gmailr_token /srv/roreviewapi/data/
sudo chmod 600 /srv/roreviewapi/data/client_secret_<id>.apps.googleusercontent.com.json
sudo chmod 700 /srv/roreviewapi/data/gmailr_token
sudo chmod 600 /srv/roreviewapi/data/gmailr_token/*

6. Verify

  • Run the test suite.
  • Add a ping editors comment in a test issue; confirm delivery from [email protected].
  • Click a tracking link; confirm the notify-email arrives via gmail_send().

8. Transfer maintenance

This section covers the Gmail-specific parts of transferring maintenance. Access, tokens for other services, the cron job, and the general redeployment steps are in the admin setup vignette.

Operations only (a new person deploys/debugs, auth stays as $USER_EMAIL) — nothing about the token changes. The only Google step is to add them to the ro-auto-email Google Cloud project’s IAM; give them access through 1Password to [email protected].

  1. Add the new maintainer to the ro-auto-email Google Cloud project’s IAM, so they can download the client_secret_....json. Because the consent screen audience is Internal, their account must belong to the rOpenSci Google Workspace; an account outside it cannot authorize the app.

Editor search endpoints

Four endpoints support the volunteer editor search workflow. All require the shared secret token.

GET /click/<token>

Records a volunteer response. Returns an HTTP 200 confirmation page, an “already used” page on duplicate clicks, or an “expired” page if the search has been deactivated. Sends a notification email to the current editor-in-chief on the first valid click.

No authentication required — the token itself is the credential.

GET /list_searches

Returns a data frame of all active and inactive searches with recipient totals and click counts. Use this to find the repourl value needed to deactivate a search.

Parameters: secret.

Notify email cache

At startup, serve_api() fetches the current editor-in-chief email address from AirTable and writes it to /data/email/notify_email.txt (alongside the SQLite database). This cache is refreshed every 24 hours via a background timer. If the AirTable call fails, the existing cached value is preserved and an error is logged.

The cached address is used as the notification recipient whenever a volunteer clicks a search link.