# Gmail API — `sendMail.php`

Sends an HTML email from **iperfect.net@gmail.com** via the Gmail API (OAuth2).

`sendMail.php` is a thin HTTP wrapper: it builds an HTML body from the request
parameters and shells out to `sendemail-x64`, a self-contained Linux binary that
holds the OAuth credentials and talks to Google.

```
Endpoint: https://services.iperfect.net/api/gmail-api/sendMail.php
Methods:  GET or POST  (parameters are read from $_REQUEST)
```

---

## Parameters

| Name        | Required | Description |
|-------------|----------|-------------|
| `email`     | yes      | Recipient address. Multiple recipients may be comma-separated. |
| `subject`   | yes      | Subject line. |
| `message`   | yes      | Body content. **HTML is allowed** and is injected as-is into the template. |
| `greetings` | yes      | Salutation line rendered in a `<p>` above the message, e.g. `Dear Sir/Madam,`. |
| `attachment` | no      | One or more files to attach — an uploaded file, a filename in the server's `attachments/` directory, or an http(s) URL. See [Attachments](#attachments). |
| `format`    | no       | `json` for a machine-readable response. See [Response](#response). |
| `debug`     | no       | `1` to include the binary's raw output and the audit-log outcome. |

All four values are inserted into the template below, so `message` and
`greetings` may contain markup:

```html
<html>
<style> #green{...} #white{...} #red{...} #orange{...} </style>
<body>
<p> {greetings} </p>
{message}
<div style="...font-size:10px;color:#888888;">Powered by <a href="https://iperfect.net">IPerfect.Net</a></div>
</body>
</html>
```

Every message ends with the **Powered by IPerfect.Net** footer — 10px grey
Verdana above a thin rule. It is styled inline rather than through the
stylesheet above, because a number of mail clients drop `<style>` blocks and a
signature that loses its formatting looks worse than one that never had any.
Edit or disable it with `$IP_MAIL_FOOTER` at the top of `sendMail.php`:

```php
$IP_MAIL_FOOTER = '';   // no footer
```

The stylesheet ships with four ready-made ids you can use inside `message`:
`#green`, `#white`, `#red`, `#orange` (all Verdana 12px) — e.g.
`<span id="red">Alert!</span>`.

---

## Attachments

Optional. Omit `attachment` entirely and everything behaves exactly as before.
Three ways to supply a file, in order of increasing trust required — all use the
same parameter name, and several may be combined in one request:

### 1. Upload it (always enabled)

`multipart/form-data`, field name `attachment` — or `attachment[]` for several.
The recipient sees the original filename.

```bash
curl -X POST https://services.iperfect.net/api/gmail-api/sendMail.php \
  -F "email=neerajdhekale@gmail.com" \
  -F "subject=Daily report" \
  -F "greetings=Dear Sir/Madam," \
  -F "message=Report attached." \
  -F "attachment=@/path/to/daily.xlsx"
```

Several files:

```bash
  -F "attachment[]=@/path/to/daily.xlsx" -F "attachment[]=@/path/to/chart.png"
```

### 2. A file already on the server

Drop the file in the `attachments/` directory next to `sendMail.php` and pass
its name. Works over GET or POST:

```
...sendMail.php?email=...&subject=...&greetings=...&message=...&attachment=daily.xlsx
```

Only files under the roots listed in `$IP_ATTACH_DIRS` (top of `sendMail.php`)
can be reached; `../`, symlinks and absolute paths that climb out are rejected.
Add more roots there if reports are written elsewhere:

```php
$IP_ATTACH_DIRS = array(__DIR__ . '/attachments', '/var/www/climate.trackany.live/send_reports/store');
```

> **The file is deleted once the mail is sent** (see below). Point
> `$IP_ATTACH_DIRS` at staging directories only — never at a live web folder
> whose files something else still serves.

### 3. An http(s) URL — **currently enabled**

```
...&attachment=https://example.com/reports/daily.pdf
```

Toggled by `$IP_ATTACH_ALLOW_URL` in `sendMail.php`. Keep in mind what it opens
up: the endpoint has no authentication, so with URL attachments on, any caller
can make the server fetch a URL only the server can reach and have the contents
mailed to an address of their choosing. Pair it with an IP allowlist or a shared
secret. Redirects are not followed, and the remote file is never modified — only
the temp copy is deleted after the send.

### Limits

Set at the top of `sendMail.php`:

| Setting | Default | Meaning |
|---------|---------|---------|
| `$IP_ATTACH_MAX_FILES` | 5 | Files per message. |
| `$IP_ATTACH_MAX_BYTES` | 10 MB | Per file. |
| `$IP_ATTACH_MAX_TOTAL` | 20 MB | Per message (Gmail refuses over ~25 MB). |
| `$IP_ATTACH_URL_TIMEOUT` | 20 s | Per URL download. |
| `$IP_ATTACH_DELETE_AFTER_SEND` | `true` | Delete path-based attachments after a successful send. |
| `$IP_ATTACH_ALLOW_URL` | `true` | Allow `attachment=https://…`. |

PHP's own limits apply first: uploads bigger than `upload_max_filesize`, or a
request bigger than `post_max_size`, never reach the script. Raise both if you
want the full 20 MB — e.g. in `.htaccess`:

```apache
php_value upload_max_filesize 25M
php_value post_max_size 26M
```

### Errors

A rejected attachment aborts the whole request — **HTTP 400 and no mail at
all**, rather than a report mail that quietly arrives without its report:

```
Returned with status 1

  Result  : REJECTED - nothing was sent
  Problem : daily.xlsx: not found in the allowed attachment directories
```

### What happens to the file afterwards

Uploads and URL downloads go to a private per-request temp directory and are
deleted once the binary has read them, whether the send succeeded or not —
nothing else can reach them.

Path-based attachments are deleted too, but only **after Gmail accepted the
mail** (`status 0`). A failed send leaves the file in place so the same request
can simply be retried. The response says which files went:

```
  Attachments : daily.xlsx (12.4 KB) - deleted after send
```

If the web user can't remove it — deletion needs write permission on the
**directory**, not just the file — the send still counts as successful and the
response says so under `Notes`:

```
  Notes       : Attachment could NOT be deleted (check directory permissions): /var/www/gmail-api/attachments/daily.xlsx
```

Set `$IP_ATTACH_DELETE_AFTER_SEND = false` to keep every file, e.g. if the same
report is mailed to several recipients in separate calls.

---

## Examples

### GET (browser / quick test)

```
https://services.iperfect.net/api/gmail-api/sendMail.php?email=neerajdhekale@gmail.com&message=test&subject=test&greetings=Feedback:
```

### GET with HTML in the body

Everything must be URL-encoded. `<b>Sensor 4</b> is offline` becomes:

```
https://services.iperfect.net/api/gmail-api/sendMail.php?email=neerajdhekale@gmail.com&subject=Device%20Alert&greetings=Dear%20Sir%2FMadam%2C&message=%3Cb%3ESensor%204%3C%2Fb%3E%20is%20offline
```

### curl (POST — use this for long or HTML-heavy bodies)

```bash
curl -X POST https://services.iperfect.net/api/gmail-api/sendMail.php \
  --data-urlencode "email=neerajdhekale@gmail.com" \
  --data-urlencode "subject=Device Alert" \
  --data-urlencode "greetings=Dear Sir/Madam," \
  --data-urlencode "message=<b>Sensor 4</b> is offline since 10:42."
```

### PHP

```php
$params = [
    'email'     => 'neerajdhekale@gmail.com',
    'subject'   => 'Device Alert',
    'greetings' => 'Dear Sir/Madam,',
    'message'   => '<b>Sensor 4</b> is offline since 10:42.',
];

$ch = curl_init('https://services.iperfect.net/api/gmail-api/sendMail.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
```

### JavaScript

```js
await fetch('https://services.iperfect.net/api/gmail-api/sendMail.php', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    email: 'neerajdhekale@gmail.com',
    subject: 'Device Alert',
    greetings: 'Dear Sir/Madam,',
    message: '<b>Sensor 4</b> is offline since 10:42.',
  }),
});
```

---

## Response

Plain text by default, `Content-Type: text/plain`. The first line always carries
the exit status — **`0` means Gmail accepted the mail**, anything else means it
failed.

```
Returned with status 0

  Result      : SUCCESS - mail accepted by Gmail
  To          : neerajdhekale@gmail.com
  Subject     : test
  Attachments : root.txt (590 B) - deleted after send
  Message ID  : <9475fb3f-ecce-c5bf-af8a-77e7356492d0@gmail.com>
  Gmail reply : 250 2.0.0 OK 1787046292 d75a77b69052e-52db6216c51sm34352161cf.14 - gsmtp
```

Failure — the binary's raw output is appended automatically, since that is where
the diagnosis lives:

```
Returned with status 1

  Result  : FAILED - mail was not sent
  To      : neerajdhekale@gmail.com
  Subject : test
  Error   : Token refresh failed (HTTP 400): invalid_grant

  Raw output from sendemail-x64:
  | --- Arguments ---
  ...
```

A rejected request (bad attachment) returns HTTP 400 and sends nothing:

```
Returned with status 1

  Result  : REJECTED - nothing was sent
  Problem : root.txt: not found in the allowed attachment directories
```

### Options

| Parameter | Effect |
|-----------|--------|
| `format=json` | Returns JSON (`Content-Type: application/json`) instead of text — `exit_code`, `status`, `result`, `to`, `subject`, `attachments`, `message_id`, `gmail_reply`. |
| `debug=1` | Also appends the binary's raw output on success (it is always shown on failure), and reports whether the `email_send_log` row was written. |

`sendemail-x64` echoes the entire HTML body back on stdout, which is why the raw
output is hidden unless it is useful — the summary above is parsed out of it.

---

## Logging

Every call is recorded in **`common.email_send_log`** — one row per attempt,
successes and failures alike. A request rejected before the send (a bad or
unreachable attachment, an oversized POST) is logged too, with `exit_code` `1`,
`status` `failed`, and the rejection reason in `response` as
`Email FAILED: <reason>`.

| Column | Contents |
|--------|----------|
| `sent_at` | Server time of the request. |
| `referer` | `HTTP_REFERER` of the caller (`NULL` if the client sent none). |
| `client_ip`, `user_agent` | `REMOTE_ADDR` and `HTTP_USER_AGENT`. |
| `to_email`, `subject`, `greetings`, `message` | Exactly what was submitted. `message` is the raw parameter, before the HTML template is wrapped around it. |
| `exit_code` | Binary exit status — `0` = accepted by Gmail. `1` on a request rejected before the binary ran. |
| `status` | `success` / `failed`, derived from `exit_code`. |
| `response` | Full stdout/stderr of `sendemail-x64`, prefixed with an `Attachments: name (size) [deleted], ...` line when the message carried any. For a rejected request, the `Email FAILED: <reason>` lines instead. |

### Setup

1. Create the table (run once, manually, on the server):
   ```bash
   mysql -u <user> -p common < email_send_log.sql
   ```
2. Fill in the credentials in `dbconfig.php`. **Logging stays off until you do** —
   if the file is missing or `IP_DB_USER` is blank, the insert is skipped.

Logging never blocks or breaks a send: connection and insert errors are
suppressed, because by the time the row is written the mail has already gone out.

### When no rows appear

The response tells you. A send whose row was not written says so:

```
  Log         : row NOT written to email_send_log - add &debug=1 for the reason
```

Add `&debug=1` for the actual cause:

| `Log` line | Fix |
|------------|-----|
| `skipped - dbconfig.php is missing from /var/www/...` | `dbconfig.php` is deliberately not in version control, so it does not travel with a deploy. Create it on the server. |
| `skipped - IP_DB_USER is blank in dbconfig.php` | Fill in the credentials. |
| `skipped - the mysqli extension is not installed` | `php-mysqli` is missing from the server's PHP build. |
| `failed to connect to common@localhost as common - Access denied ...` | Wrong credentials, or the user has no access from this host. |
| `prepare failed - Table 'common.email_send_log' doesn't exist` | Run `mysql -u <user> -p common < email_send_log.sql`. |
| `prepare failed - INSERT command denied to user ...` | Grant `INSERT` on `common.email_send_log`. |
| `written - common.email_send_log row id 41` | It worked; the row is there. |

Two things worth knowing while hunting for missing rows:

- **Rejected requests are not logged.** A bad attachment returns HTTP 400 before
  the binary runs, so nothing is written — the table records send *attempts*,
  not every hit on the endpoint.
- **`referer` is normally `NULL`.** Browsers and `curl` do not send
  `HTTP_REFERER` on a direct address-bar hit or a plain command-line call; it
  only appears when the request came from a link or a form on a page. That is a
  property of the caller, not a bug in the logging.

### Counting sends per referer

```sql
SELECT COALESCE(referer,'(none)') AS referer,
       COUNT(*)             AS total,
       SUM(status='success') AS sent,
       SUM(status='failed')  AS failed
  FROM common.email_send_log
 GROUP BY referer
 ORDER BY total DESC;
```

More ready-made queries (daily volume, recent failures, top recipients) are in
the comments at the bottom of `email_send_log.sql`.

---

## Notes and limitations

- **No authentication.** Any caller who knows the URL can send mail as
  `iperfect.net@gmail.com`. Restrict it (IP allowlist, shared secret, or a
  server-side-only call) before exposing it anywhere public.
- **Apostrophes.** The binary decodes HTML entities and converts `&#39;` back to
  `'`, so encoded quotes in `message` survive correctly.
- **Attachments** are optional and off unless the caller asks for them — see
  [Attachments](#attachments). Uploads work out of the box; server paths are
  restricted to `$IP_ATTACH_DIRS`; URL fetching is enabled. A request never
  reaches the binary with a path taken raw from user input.
- **Sending consumes the file.** A path-based attachment is deleted once the
  mail goes out, so one call = one send. Mailing the same file twice means
  staging it again, or turning `$IP_ATTACH_DELETE_AFTER_SEND` off.
- **`text` fallback.** The plain-text alternative part is hardcoded to `"test"`
  in the binary; only the HTML part carries real content.
- **Access token** is refreshed on every call and is valid ~1 hour; nothing is
  cached between requests.

---

## Files

| File | Purpose |
|------|---------|
| `sendMail.php` | HTTP wrapper — builds the HTML body, shells out to the binary, writes the audit row. |
| `sendemail-x64` | Linux x86-64 binary (Node 22, compiled with `pkg`). Must be executable (`chmod +x`). |
| `dbconfig.php` | `common` database credentials. Edit on the server; keep out of version control. |
| `email_send_log.sql` | Schema for the audit table, plus reporting queries. Run manually. |
| `attachments/` | Staging directory for path-based attachments; files here are deleted after a successful send. Ships with an `.htaccess` that blocks web access — keep it, the files in here are meant to be mailed, not served. |

### Rebuilding the binary

Source lives outside this repo at `IOT/NodeJS/GMail API/SendEmail`:

```bash
cd "IOT/NodeJS/GMail API/SendEmail"
npm install
npm run build          # writes dist/sendemail-x64 and dist/sendemail-arm64
cp dist/sendemail-x64 /var/www/gmail-api/sendemail-x64
chmod +x /var/www/gmail-api/sendemail-x64
```

Use `sendemail-arm64` instead if the server is ARM.

The binary's own CLI contract is:

```
sendemail-x64 <to> <subject> <html> [attachment paths...]
```

Empty arguments are skipped, so the placeholder `''` that `sendMail.php` passes
after the body is harmless.

> The OAuth client secret and refresh token are compiled into the binary. Anyone
> who obtains the file can extract them — keep it off public paths and rotate the
> credentials if it is ever shared.
