---
title: "A reproducible IMAP sandbox with Docker"
output:
  rmarkdown::html_vignette:
    toc: yes
vignette: >
  %\VignetteIndexEntry{A reproducible IMAP sandbox with Docker}
  %\VignetteEngine{knitr::rmarkdown}
  \usepackage[utf8]{inputenc}
---

```{r, include = FALSE}
knitr::opts_chunk$set(eval = FALSE)
```

## Introduction

Trying an IMAP client requires a mail server and a populated mailbox — which usually means real credentials, a provider's rate limits, and results that cannot be reproduced anywhere else. This vignette shows how to run `mRpostman` against a **disposable local IMAP server** (Dovecot, in a Docker container) whose mailbox is populated **deterministically by the package itself**: `populate_sandbox()` uploads a fixed synthetic corpus with the package's own `APPEND` implementation, so any user, on any machine, obtains exactly the same messages — and therefore exactly the same search, fetch, and decoding results shown here.

This is useful for:

* trying every feature of the package without a real mail account (no OAuth2 setup, no "less secure apps" hurdles);
* teaching and demonstrations, with reproducible outputs;
* testing code that uses `mRpostman` against a fast, offline, rate-limit-free server.

None of the code chunks in this vignette are evaluated when the package is built (there is no Docker server on CRAN's machines); copy and paste them into your session.

**NOTE**: the sandbox server is for local experimentation only — it uses plaintext authentication, no TLS, and a static password.

## Starting the server

You will need [Docker](https://docs.docker.com/get-docker/). The `Dockerfile` and the Dovecot configuration ship with the package; find them with:

```{r}
system.file("docker", package = "mRpostman")
```

From that folder, build the image and start a container, exposing the IMAP port as `1430` on the host:

```{sh}
docker build -t mrpostman-sandbox .
docker run -d --name mrpostman-sandbox -p 1430:143 mrpostman-sandbox
```

The server accepts **any username** with the password `sandbox`, and each username gets its own empty mailbox.

## Connecting

```{r}
library(mRpostman)

con <- configure_imap(url = "imap://localhost:1430",
                      username = "testuser",
                      password = "sandbox",
                      use_ssl = FALSE)

con$list_server_capabilities()
```

Note the `use_ssl = FALSE` and the `imap://` (not `imaps://`) scheme: the sandbox has no TLS. Dovecot advertises, among others, the `ESEARCH`, `SORT`, `THREAD`, `MOVE`, `UNSELECT`, and `SPECIAL-USE` capabilities, so the package's extension-based methods can be exercised too.

## Populating the mailbox

`populate_sandbox()` generates the synthetic corpus of `sandbox_corpus()` — 200 RFC 822 messages by default, driven by a fixed RNG seed — and stores it using the package's own IMAP operations (`APPEND`, `CREATE`, `STORE`):

```{r}
info <- populate_sandbox(con, n = 200)
#> Appending 200 synthetic messages to INBOX at imap://localhost:1430 ...
#>   50/200
#>   100/200
#>   150/200
#>   200/200
#>
#> Done. INBOX now reports:
#> $EXISTS
#> [1] 200
#> ...
```

The returned (invisible) `info` data.frame describes what was embedded in each message, so every result below can be checked against it:

```{r}
head(info)
#>   id                     from                  subject ...
#> 1  1 felipe.duarte@example.com     Meeting minutes     ...
```

The corpus is designed so that each feature of the package has matching messages to act upon:

* `Date:` headers spread over the year **2020**, for the `SENT*` date searches;
* a subset of large bodies, for size searches;
* accented subjects in MIME encoded-words and quoted-printable bodies, for the decoding helpers;
* CSV, PNG, and one-page PDF attachments (some with repeated filenames), for the attachment methods, binary base64 decoding, and filename deduplication — see `info$attachment_type`;
* reply chains (`In-Reply-To`/`References`), for `SORT` and `THREAD`;
* `\Seen`, `\Flagged`, and `\Answered` flags, adjusted after the upload, for flag searches. (libcurl's `APPEND` stores every message with the `\Seen` flag, so `populate_sandbox()` *removes* it from the messages planned as unread — exercising `STORE` in both directions.)

For use from the shell, the wrapper script `inst/docker/populate_mailbox.R` does the same: `Rscript populate_mailbox.R 200`.

### A note on dates

`APPEND` stores messages with the *current* internal date. Searches on the **internal date** (`search_before()`, `search_since()`, `search_on()`, `search_older_than()`, `search_younger_than()`) therefore relate to the moment you populated the mailbox. Searches on the **`Date:` header** (`search_sent_before()`, `search_sent_since()`, `search_sent_on()`, `search_sent_period()`) act on the fixed 2020 dates of the corpus and always return the same results.

## Ingesting real data: maildirs and the Enron corpus

The synthetic corpus is not the only way to fill the sandbox. `ingest_maildir()` uploads any local maildir-style directory (one RFC 5322 message per file) to a server folder via `APPEND` — mail server backups, exported archives, or public corpora:

```{r}
manifest <- ingest_maildir(con, path = "~/backup/maildir/archive2020",
                           folder = "archive2020")
table(manifest$appended)
```

Built on top of it, `enron_sandbox()` turns the sandbox into a real-data laboratory: it downloads the public Enron e-mail corpus once (~423 MB, with your consent, cached under `tools::R_user_dir("mRpostman", "cache")`), selects a subset by custodian, folder-name pattern, and `Date:` header window, and ingests it — one server folder per custodian:

```{r}
con2 <- configure_imap(url = "imap://localhost:1430",
                       username = "enron", password = "sandbox",
                       use_ssl = FALSE)
manifest <- enron_sandbox(con2,
                          custodians  = c("kaminski-v", "lay-k", "skilling-j"),
                          sent_since  = "2000-01-01",
                          sent_before = "2002-07-01")
table(manifest$custodian, manifest$appended)
```

After the ingestion, everything shown in the guided tour below works on real data too — e.g. `con2$search_string(expr = "SEC", where = "TEXT")` on a custodian's folder. The same internal-date caveat applies: query ingested corpora by date through the `SENT*` searches, which read the `Date:` header.

## A guided tour

### Mailbox structure

```{r}
con$list_mail_folders()
con$examine_folder("INBOX")
con$select_folder("INBOX")
```

Besides `INBOX`, `populate_sandbox()` creates a `ProjectAtlas` folder with a small subset of the corpus, for folder, `copy_msg()`, and `move_msg()` demonstrations.

### Searching

Single-criterion searches:

```{r}
# messages "sent" in the first quarter of 2020 (Date: header)
ids_q1 <- con$search_sent_period(since_date_char = "01-Jan-2020",
                                 before_date_char = "01-Apr-2020")

# large messages
ids_large <- con$search_larger_than(size = 8000)

# flagged messages
ids_flagged <- con$search_flag("FLAGGED")
```

Composable custom searches:

```{r}
ids <- con$search(request = OR(string(expr = "sales", where = "SUBJECT"),
                               string(expr = "alice.fontes@example.com",
                                      where = "FROM")))
```

The `ESEARCH` extension can condense results server-side:

```{r}
con$esearch_count(flag = "SEEN")
con$esearch_min_id(flag = "FLAGGED")
```

### Fetching and decoding

```{r}
meta <- ids_q1[1:5] %>%
  con$fetch_metadata(attribute = c("INTERNALDATE", "RFC822.SIZE"))

texts <- ids_q1[1:5] %>%
  con$fetch_text()
```

The corpus includes quoted-printable bodies and MIME encoded-word subjects with accented characters — fetch one of the messages flagged as `is_utf8_body` in `info` and decode it with `clean_msg_text()`:

```{r}
utf8_ids <- info$id[info$is_utf8_body]
raw_text <- con$fetch_text(msg_id = utf8_ids[1])
clean_msg_text(raw_text)
#> [[1]]
#> [1] "Segue em anexo o relatório com os números consolidados. ..."
```

### Attachments

```{r}
att_ids <- info$id[info$has_attachment]

# list attachment filenames without downloading
con$fetch_attachments_list(msg_id = att_ids[1:5])

# download them (note the automatic deduplication of repeated filenames)
con$fetch_attachments(msg_id = att_ids[1:5])
```

### Server-side sorting and threading

Dovecot implements the `SORT` and `THREAD` extensions (which, for instance, Gmail does not), so the sandbox is a convenient place to try them:

```{r}
con$sort(by = "SUBJECT", use_uid = TRUE)
con$thread(algorithm = "REFERENCES")
```

### Flags and mailbox management

```{r}
con$copy_msg(msg_id = ids_flagged[1], to_folder = "ProjectAtlas")
con$add_flags(msg_id = ids_flagged[1], flags_to_set = "\\Deleted")
con$expunge()
```

## Resetting and tearing down

Since the server accepts **any username** — and each one gets its own independent mailbox — the quickest way to start over is to simply connect with a new username, with no need to touch the container:

```{r}
con2 <- configure_imap(url = "imap://localhost:1430",
                       username = "testuser2", # any new name = pristine mailbox
                       password = "sandbox",
                       use_ssl = FALSE)
populate_sandbox(con2, n = 200)
```

This is also handy for keeping several corpora side by side (e.g. one mailbox per experiment or per `seed`).

Alternatively, the mailboxes live inside the container, so recreating it resets everything at once:

```{sh}
docker rm -f mrpostman-sandbox
docker run -d --name mrpostman-sandbox -p 1430:143 mrpostman-sandbox
```

Either way, a new `populate_sandbox()` call recreates the exact same corpus.
