A Practical Way to Archive Office 365 Mail with Thunderbird and GYB

I began with a practical deadline: my Office 365 mail account was going to be deleted, and I needed to preserve the messages before access disappeared. I wanted the attachments as well as the text, and I wanted an archive I could continue to use. My Mac had limited free space, while my Google account had ample storage. At first, this sounded like a straightforward transfer between two places that both understood email.

My opening question was which program would be best. I asked about Thunderbird, imapsync and other open-source tools, expecting that choosing the right one would settle most of the problem. I did not begin with the workflow described here. It took shape through an exchange with an AI assistant in which a proposed solution repeatedly met something it had not yet accounted for: my storage constraint, a screen that differed from the instructions, an error followed by a success message. Each time I asked what to do next, the answer depended on reconsidering what the previous step had actually established.

The constraints inside a simple request

My first preference was to keep the backup on an external disk or online. I later accepted a local download because the Office 365 mailbox appeared modest in size. This was a change in the plan, but it had a specific boundary. I was willing to store the source mail temporarily; I still wanted to avoid downloading my much larger existing Gmail mailbox. That distinction became consequential when the conversation reached the destination side of the transfer.

The distinction mattered because the conversation repeatedly drifted toward technically plausible answers to slightly different questions. Storing a backup file in Google Drive would give me an online copy, but the messages would not become searchable mail inside Gmail. Adding Gmail to Thunderbird could support the transfer, but it introduced synchronization settings I wanted to avoid managing. A custom Python uploader could send the saved messages, but I first wanted to know whether an existing open-source application could do the job.

I also asked for a graphical interface, then accepted command-line work when it offered a more controlled route. These preferences could look inconsistent if separated from the problem. Together they made the requirement clearer: I wanted to preserve the mail without setting a second, unnecessary download in motion. The assistant needed that distinction to choose an appropriate workflow; a general recommendation for a good mail client was insufficient.

Getting access without controlling the platform

The first serious candidate was imapsync, which can copy messages between IMAP accounts. My machine was an Apple Silicon Mac running macOS 12. I wanted to avoid a lengthy Homebrew installation, so we tried a standalone executable. The download completed, but running it produced a Perl loading error:

Attempt to reload DynaLoader.pm aborted.
Compilation failed in require at <embedded>/XSLoader.pm line 118.

An older standalone release, imapsync 2.200, did run and report its version. I returned that successful output and asked for the next step. At that moment, the installation problem had a workable answer. Authentication soon made the limit of that success apparent: a program could start without yet being able to read my account. The original loading error was not investigated far enough to justify a precise claim about its underlying cause.

Following the assistant’s proposed route, I opened Microsoft Entra and encountered a 401 access error. I supplied another fact alongside the error: Outlook on the web still displayed my mail. That was the practical contradiction I needed the assistant to address. I could read the account, yet the proposed setup route led to a page I could not enter. Treating the 401 as the end of the inquiry would have discarded the evidence of continuing access. We needed another client route; we had not established that exporting the mail was prohibited.

Microsoft documents application registration, permission scopes and access tokens for OAuth-based IMAP connections. Imapsync’s Office 365 guidance likewise distinguishes having IMAP access from obtaining the authorization needed to use it. Those are separate requirements, and a working webmail session does not resolve them for every other application. (Microsoft, n.d.) (Lamiral, 2025)

We changed direction and connected Thunderbird through its Microsoft account setup, selecting Exchange Web Services and OAuth2. Mail appeared in the client. This gave us a working way forward without resolving the earlier IMAP authorization problem. In retrospect, that distinction matters: the evidence justified continuing with Thunderbird, while leaving the exact limits of the imapsync route unsettled.

Seeing mail was only the beginning

Once Thunderbird displayed the messages, I checked that they appeared to correspond to the source and asked whether everything would be saved, including the original attachments. Access had been the immediate obstacle; now it was tempting to treat visible mail as the result I needed. My question about attachments exposed the gap. I needed the messages to survive the disappearance of the account, and displaying them while connected could not establish that. The practical step was to copy messages into Thunderbird’s Local Folders, leaving the source account intact.

Mozilla’s IMAP documentation explains why a visible mailbox and a complete local copy should not be treated as interchangeable. A client can hold headers while fetching message bodies on demand; synchronization can also propagate changes between client and server. Although my working source connection used Exchange Web Services, this documented IMAP behaviour clarified the broader concern behind the proposed Gmail synchronization route. (Mozilla, 2015)

After the copy, I asked whether it was already done because I could not see a progress bar. The local Inbox properties showed 933 messages and approximately 113 MB on disk. Later, the saved folder tree occupied about 268 MB. These observations offered a different kind of evidence from an animated indicator: a count and stored data that could be examined. They still left open whether every source folder had been included and whether the saved attachments could all be opened.

A yellow warning about blocked remote content then challenged my understanding of “everything.” I asked whether Thunderbird should download it all instead of blocking it, and I enabled remote content. My question grouped together things that looked like parts of the same email on screen. The correction was that some of those things lived on external websites. Mozilla describes remote content as resources fetched when a message is viewed, rather than content contained in the message itself. Blocking those resources did not, by itself, mean the attached files were missing; enabling them did not make the archive complete. (Mozilla, 2025)

This changed what I could reasonably expect from copying the mail. An attached PDF travels inside the message. A link to a document in OneDrive or another online service remains a reference to something elsewhere. Preserving the message does not establish that the linked resource will remain accessible. I still wanted everything preserved, but the word now contained separate tasks: saving the message and, where necessary, obtaining the external material it referred to.

A one-way route into Gmail

Once the local copy existed, I asked to import it into Gmail as another archive. The assistant proposed adding Gmail to Thunderbird and copying between accounts. When I raised the storage concern, it proposed disabling offline downloading. That addressed the risk in the suggested method, but I kept asking whether I could upload the saved mail without adding Gmail to Thunderbird at all. I had not proved that the Thunderbird method would fill the disk. I was specifying which moving parts I wanted to avoid.

That objection changed the next proposal. The assistant suggested using “the download you’ve already completed” and avoiding “solving Microsoft authentication again.” This was the useful connection: Thunderbird had already solved source access, so another tool could begin with its files. My earlier search for a program that would connect Office 365 directly to Gmail was now too restrictive. The two stages could use different tools.

The proposed importer was Got Your Back, or GYB. Its documented restore-mbox action accepts MBOX and EML files, including files obtained outside GYB’s own backup process. Here, “restore” meant importing saved Office 365 mail into Gmail. GYB’s default action is backup, so explicitly selecting --action restore-mbox expressed the direction I wanted. (GAM-team, 2024)

GYB 1.95’s standalone Apple Silicon package downloaded successfully, passed the checksum comparison in the installation command and ran on macOS 12.7.6. Its version output reported a bundled Python 3.13.7 environment. We therefore had direct evidence of compatibility on my machine, rather than another assumption based on the operating system’s age.

There was still Google-side setup. I created a project, configured an OAuth consent screen, selected an External audience with my own account as a test user, and created a Desktop app client. GYB’s printed instructions described an older interface, so the assistant had to map them onto the screens I actually saw. Google’s current documentation describes the Branding and Audience configuration and the addition of test users. (Google, 2026a)

The permissions menu made the same question concrete again. It initially selected broad Gmail access and additional options for Groups and storage quota. I posted the menu, asked which options to choose, and selected only “Gmail Restore Only.” The resulting permissions allowed adding messages and managing labels without authorizing the reading of my existing message bodies. My repeated storage objection now had a counterpart in both the command’s action and its authorization. Google’s scope definitions distinguish gmail.insert from broader mailbox permissions, while gmail.labels permits label access. (Google, 2026b)

We first ran --action print-labels. When the existing Gmail labels appeared in Terminal, that established that authorization worked for the requested operation. No message transfer had yet occurred. It was a useful checkpoint precisely because its result was limited and intelligible.

By this point, the repeated setup decisions were wearing thin. When the assistant proposed further preliminary steps, I asked it to stop overcomplicating the task and save all the mail already downloaded into Gmail. That objection changed the pace of the work. We proceeded to the full upload; later errors still had to be reported and reconciled. Wanting fewer preparatory steps did not make a skipped message acceptable.

Preparing the upload without duplicating the archive

Our description of the route had been “Thunderbird to MBOX to GYB to Gmail.” The implementation became simpler than that wording suggested. The saved Thunderbird folders were already MBOX files. They did not need another complete export merely to acquire an extension GYB recognized.

The assistant wrote a Python wrapper that inspected the saved folder tree, ignored index files, and created a separate staging directory for each mailbox. Each staging directory contained a symbolic link named mailbox.mbox pointing to the existing file. This avoided duplicating the full archive. It also meant the staging directories were not additional backups: they depended on the original files remaining available.

The wrapper mapped the source folder names to Gmail labels and invoked GYB separately for each mailbox. The following excerpt shows the operative arguments from the later run; the variables were supplied by the wrapper. Account details and archive labels in this article are anonymized.

command = [
    str(gyb), "--email", email,
    "--action", "restore-mbox",
    "--local-folder", str(folder),
    "--label-restored", label,
    "--strip-labels", "--batch-size", "10",
]

The labels retained the useful distinction between Inbox, Drafts and Sent under a common archive label. The --strip-labels argument suppressed imported label metadata while the explicit archive label was applied. This was a mail archive, rather than an attempt to reproduce every piece of account state. For example, a label ending in /Drafts did not by itself establish that Gmail had recreated those messages as editable native drafts.

The instructions were to keep Thunderbird closed during the upload. The links referenced the source files, and the resume mechanism depended on stable files and staging paths. Saving disk space through links therefore introduced a condition we had to respect: the staging area did not freeze the mailboxes into snapshots.

The five drafts that “done!” did not include

The initial upload found three saved mail folders. While processing Drafts, it produced five instances of the same error:

ERROR: 400: Expected a single 'From' header. Skipping message restore.

I returned the output and asked whether it was correct. The transfer was continuing, but these messages were explicitly being skipped. Missing or malformed sender headers were plausible explanations for unfinished drafts; the error did not identify the exact defect in each original. We had enough evidence to identify a failed import, without enough to reconstruct every header problem.

GYB then printed done!. Before the errors, reaching the end of a folder had seemed a reasonable sign of completion. The combination of the error and the final word made that reading untenable. Processing could finish with messages still absent from the destination. The version-specific source makes the distinction checkable: these 400 responses are reported and skipped, while successful responses enter the restore records. (GAM-team, n.d.)

We eventually retried the outstanding drafts with --cleanup and an explicit fallback sender. GYB’s cleanup logic can repair or supply message identifiers, dates and sender headers. The retry produced five restores without the earlier errors, and the final Drafts count reached 33 recorded uploads for 33 local messages. (GAM-team, n.d.)

The repair also complicated my earlier request for the originals. A fallback sender can make a draft importable, but it cannot recover an unknown historical sender. Generated dates or identifiers would carry the same limitation. I cannot infer from the successful retry which fields changed in every draft. The cleanup operated on the upload representation and left the local MBOX files unchanged. Retaining those files now had an additional purpose: they preserved the source form alongside Gmail’s usable, potentially normalized copies.

Recovering from a stalled upload

I also challenged the speed. Why should a few hundred megabytes take so long? My question treated the archive like a single file upload, where total size would be an obvious guide to duration. The command made that picture incomplete: it specified --batch-size 1, so small messages incurred separate requests. Google’s API documentation explains that batching reduces HTTP overhead, while warning that large batches can trigger rate limits. (Google, 2026c)

We later increased the batch size to ten. The upload progressed after that change, but I did not measure a controlled before-and-after rate. There was also no evidence sufficient to claim that Google throttling had caused the original slowness. The justified explanation was narrower: the initial configuration introduced avoidable per-request overhead.

A more immediate interruption occurred when the Inbox progress display remained at message 756 and 85.21%. I reported that the Mac had gone to sleep, then returned to say the process was still blocked. That second observation mattered: preventing future sleep would not, on its own, establish that the existing upload had recovered. Sleep was a plausible explanation for the interruption, but the record contains no network trace proving the cause. I stopped the process with Control+C; the resulting KeyboardInterrupt showed that the wrapper had been interrupted.

The restart ran under macOS’s caffeinate -i, which inhibits idle system sleep while the associated command runs. This addresses idle sleep rather than every possible interruption; keeping the laptop open and connected remained part of the working conditions. (Apple, 2012)

The restart also reused the existing staging paths and restore records. GYB skipped recorded uploads and finished the Inbox processing. This was evidence that resuming could preserve useful progress in this run. It was not an independent duplicate audit: a remote acceptance and its local checkpoint remain separate events. More immediately, the resumed output raised a problem that the successful Inbox recovery did not answer.

The folder the restart never saw

The first resume script showed Inbox and Drafts, repeated the five draft errors, and ended with “Finished processing all folders.” I brought that output back to the assistant and asked what came next. Comparing it with the first run exposed a second problem alongside the rejected drafts: the original upload had announced three saved folders, but the restart had named only two. Sent was missing.

The restart had seemed reasonable because it reused the files and state created during the interrupted run. The defect was in treating that state as the complete list of work. The assistant had written the restart to enumerate existing staging links, while the original wrapper created each mailbox’s link only when it reached that mailbox. Stopping in Inbox meant Sent had never been prepared. The restart could finish everything it discovered and still omit an entire saved folder.

This was a defect in the wrapper generated during our conversation, not evidence of a GYB folder-discovery failure. The relevant contrast was visible in two expressions: state.glob("*/mailbox.mbox") searched the accumulated staging state, whereas source.rglob("*") returned to the saved source tree. The corrected wrapper enumerated the source again, created any missing links and reused the existing restore records.

This changed the question more sharply than the stalled progress bar had. “Can the upload continue?” had an answer: yes, recorded work could be skipped. “Has every saved folder entered the process?” required a different check against the source. I had supplied the output and kept the question of completion open; the comparison and code correction made its significance explicit. The new script needed both the original inventory and the accumulated progress records. Either one alone was insufficient.

What the final numbers establish

Two misleading endings had now required correction: done! after rejected messages, and “all folders” after enumerating an incomplete staging area. The final check grew directly out of those failures. The corrected wrapper returned to the saved source tree, retried the outstanding drafts and included Sent. It then counted messages in each local MBOX and compared those counts with the corresponding GYB restore records. With the archive label anonymized, the result was:

UPLOAD RESULTS:
Office 365 Archive/Drafts: 33 recorded uploads / 33 local messages
Office 365 Archive/Inbox: 933 recorded uploads / 933 local messages
Office 365 Archive/Sent: 143 recorded uploads / 143 local messages

That is a total of 1,109 local messages with matching recorded uploads. It is stronger evidence than a progress percentage or an unconditional done!. It also corrects a potential misreading of the earlier progress output: counters such as 34 or 144 at the end of processing were not the verified folder totals. The explicit local counts were 33 and 143.

I would describe the result carefully: the three saved mailboxes had matching source counts and successful-upload records, with no reported errors in the final run. I would not turn this into a claim that every item ever held by the Office 365 account had been preserved. Other folders still required checking against the original account. Contacts, calendars and linked cloud documents were outside the demonstrated transfer.

The count comparison also brought my early concern about attachments back into view. The workflow submitted full messages, including embedded attachments, but counting successful-upload records does not verify every destination attachment byte for byte. Opening representative messages and attachments in Gmail remained a useful next check; the conversation does not confirm it was completed. The limited Gmail permissions reflected my decision to avoid reading the existing mailbox, so the final script checked local records rather than retrieving destination messages for comparison.

I began by asking for the best program. The useful answer emerged from changing that question when the evidence required it. Working access through Thunderbird made a second source connection unnecessary. My objection to downloading Gmail helped establish a separate import stage. The draft errors changed how I read completion messages, and the missing Sent folder changed where the script looked for the inventory. These were decisions produced during the work, rather than features of a plan we had possessed at the outset.

The AI assistant contributed research, commands and explanations, including a faulty restart script and its correction. I supplied constraints, screenshots and failures, and repeatedly asked whether the apparent result answered the original need. The final counts gave that exchange a concrete result: 1,109 saved messages with matching recorded uploads. My next question could therefore be narrower and more useful. Before access disappeared, what remained outside those three saved mailboxes, and could the important messages and attachments be read independently from the destination archive?

References

Apple. (2012). caffeinate manual page. Dated 9 November 2012. PowerManagement source repository. Official source. Accessed 15 September 2026.

GAM-team. (2024). Got Your Back wiki. Last edited 31 October 2024. See installation, authorization, restore-mbox and restore-label options. Project documentation. Accessed 15 September 2026.

GAM-team. (n.d.). gyb.py, version 1.95. See restore-mbox processing, restored_message, cleanup_from and message_hygiene. Version-specific source code. Accessed 15 September 2026.

Google. (2026a). Configure the OAuth consent screen and choose scopes. Updated 3 September 2026. Google Workspace documentation.

Google. (2026b). Choose Gmail API scopes. Updated 10 September 2026. Gmail API documentation.

Google. (2026c). Batch requests. Updated 10 September 2026. Gmail API documentation.

Lamiral, G. (2025). Imapsync tips for Office365. Revision dated 9 December 2025. Cited for the distinction between IMAP access and OAuth authorization. Project documentation.

Microsoft. (n.d.). Authenticate an IMAP, POP or SMTP connection using OAuth. Microsoft Learn. Accessed 15 September 2026.

Mozilla. (2015). IMAP synchronization. Updated 27 October 2015. Thunderbird Help.

Mozilla. (2025). Remote content in messages. Updated 17 August 2025. Thunderbird Help.