--- title: "Editor Search Emails" author: - "Mark Padgham" date: "`r Sys.Date()`" vignette: > %\VignetteIndexEntry{Editor search emails} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set ( collapse = TRUE, warning = TRUE, message = TRUE, width = 120, comment = "#>", fig.retina = 2, fig.path = "README-" ) ``` This vignette describes how `roreviewapi` sends editor-search emails through the Gmail API, sending "from" the `reviewbot@ropensci.org` alias but authorized under a single Google account. Set that account once as an environment variable, and every later step refers back to it: ```bash export USER_EMAIL=myname@ropensci.org ``` This vignette complements the [admin setup vignette](https://docs.ropensci.org/roreviewapi/articles/admin-setup.html), 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 ## 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_.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): ```{r auth, eval = FALSE} library (gmailr) gm_auth_configure (path = "client_secret_.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. Send-as alias Add `reviewbot@ropensci.org` as a "Send mail as" address under `$USER_EMAIL`'s Gmail settings (Settings → Accounts → Send mail as). No separate authorization is needed, as the `gmail.send` scope already covers sending as any verified alias of that account. This is set via `gm_from()` on each outgoing message. ## 4. Code [`R/editor-search.R`](https://github.com/ropensci-review-tools/roreviewapi/blob/main/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. ## 5. Environment variables | Variable | Value | |---|---| | `GMAIL_SENDER` | `reviewbot@ropensci.org` | | `GMAIL_OAUTH_CLIENT_SECRET` | `/data/email/client_secret_.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](https://docs.ropensci.org/roreviewapi/articles/admin-setup.html#environment-variables); 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 6). ## 6. Deploy credentials to the droplet Copy `client_secret_.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: ```bash scp client_secret_.apps.googleusercontent.com.json youruser@droplet:~/ scp -r gmailr_token youruser@droplet:~/ # on the droplet: sudo mv ~/client_secret_.apps.googleusercontent.com.json /srv/roreviewapi/data/ sudo mv ~/gmailr_token /srv/roreviewapi/data/ sudo chmod 600 /srv/roreviewapi/data/client_secret_.apps.googleusercontent.com.json sudo chmod 700 /srv/roreviewapi/data/gmailr_token sudo chmod 600 /srv/roreviewapi/data/gmailr_token/* ``` ## 7. Verify - Call `send_search()` against a real/test issue; confirm delivery from `reviewbot@ropensci.org`. - Click a tracking link; confirm the notify-email arrives via `gmail_send()`. - Run the test suite. ## 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](https://docs.ropensci.org/roreviewapi/articles/admin-setup.html#transferring-maintenance). **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. **Authorizing identity itself** (needed if `myname@ropensci.org` becomes unusable, e.g. leaving the org). The `.env` values affected are `GMAIL_AUTH_EMAIL` (the new maintainer's address) and the contents of the `GMAIL_TOKEN_CACHE` directory (a new token). `GMAIL_SENDER` stays `reviewbot@ropensci.org`, and `GMAIL_OAUTH_CLIENT_SECRET` is unchanged as long as the same Cloud project and OAuth client are kept. 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. 2. Add `reviewbot@ropensci.org` as a "Send mail as" alias under the new maintainer's Gmail account (Section 3). 3. They run the authorization in Section 2 as themselves (with `USER_EMAIL` set to their own address), using the same `client_secret_....json`, producing their own `gmailr_token/`. The `scopes` must be unchanged from those in `gmail_auth()`. 4. Deploy their `gmailr_token/` (and `client_secret_....json`, if it changed) to `/srv/roreviewapi/data/`, replacing the old ones (Section 6). Remove the old `gmailr_token` directory first, so that stale files do not remain. 5. Update `GMAIL_AUTH_EMAIL` in the droplet's `.env` to their address. 6. Redeploy by running `restart.sh`, then verify as in Section 7. 7. Revoke the old grant from the departing account's Google Account → Security → "Third-party apps with account access". ## Editor search endpoints Four endpoints support the volunteer editor search workflow. All require the shared `secret` token. ### `GET /send_search` Fetches current editor email addresses from AirTable and GitHub, inserts a search record into the database, and dispatches personalised click-link emails via Gmail. The submission type (standard vs. stats) is determined automatically from the GitHub issue template. Parameters: `repourl`, `repo` (org/repo), `issue_id`, `secret`. ### `GET /click/` 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`. ### `GET /deactivate_search` Marks the search inactive (preventing further click responses) and permanently deletes all recipient rows and the search record from the database. Parameters: `repourl`, `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.