cmc/cleberg.net

My personal web garden & blog.

clone: git clone https://gitbay.org/cmc/cleberg.net.git

main: content/blog/2025-02-24-email-migration.org · raw

  1#+date:        [2025-02-25 Mon 19:20:05]
  2#+title:       Email Migration: 5,000+ Emails, Proton to Migadu
  3#+description: Moving 5,000+ emails from Proton Mail to Migadu.
  4#+slug:        email-migration
  5#+filetags:    :privacy:
  6
  7* The Setup
  8
  9I recently migrated my emails from Proton Mail to Migadu after a failed attempt
 10to get myself into the Proton ecosystem and wanted to detail my process, as it
 11was far more painful than expected.
 12
 13To give some context: I had nearly 5000 messages stored, accounting for around
 142.5 GB of space.
 15
 16Overall, this process would have taken all day had I done it in one sitting, but
 17I decided to break it up and it lasted a couple days before I was able to say that
 18all my messages were stored in my new account.
 19
 20* Exporting Messages
 21
 22To start, I needed to export my messages from Proton Mail. As I am using macOS,
 23I was able to use the [[https://proton.me/support/proton-mail-export-tool][Proton Mail Export Tool]]. However, the downside is that
 24this dumps every single email on your account into a single folder in the =.eml=
 25format. They also export a JSON file for each message, in the case you're
 26importing back into Proton Mail.
 27
 28This means that anything in my Inbox, Sent, Archive, Trash, and user-created
 29folders were all dumped out into a single folder with incomprehensible names.
 30
 31Without a clear path to easily figure out how to re-organize my emails into a
 32new account, I was left a bit annoyed at Proton's export process.
 33
 34* Importing Messages
 35
 36Left with a pile of messages and no way to discern what they were without
 37opening each one, I decided to try and use Thunderbird to import messages into
 38my new Migadu IMAP account.
 39
 40This led to a dead end as my two methods failed:
 41
 421. [[https://addons.thunderbird.net/en-US/thunderbird/addon/importexporttools-ng/][ImportExportTools NG]] does not work with my version of Thunderbird (135).
 432. Manually dragging the =.eml= files onto a folder in Thunderbird worked for
 44   small batches of files, but seemed to lock up if I tried to import more than
 45   a few hundred at a time. It also seemed a bit buggy, as I ended up with many
 46   duplicate, and sometimes triplicate, messages.
 47
 48At this point, I decided to take a step back and use [[https://github.com/djcb/mu][mu]], a command-line utility
 49that would index my files and sync back and forth with Migadu for me.
 50
 51Using my blog post, [[https://cleberg.net/blog/mu4e.html][Email in Doom Emacs with Mu4e on macOS]], (and skipping the
 52mu4e parts) I was able to set up a minimal directory connected to my Migadu IMAP
 53account. Using my terminal, I simply moved all of my messages into the =mu=
 54directory and synchronized the account, and voila, my messages synchronized
 55successfully to the remote server and my other email clients.
 56
 57However, the remaining issue was that I now had all 5000 messages in the Archive
 58folder and needed to figure out how to organize them back into their proper
 59directories.
 60
 61* Organizing Messages Into Folders
 62
 63As with any problem, I used Python as my hammer to fix the problem. I started by
 64creating the directories required in Thunderbird, fetching them with =mbsync= so
 65that they appeared in my =mu= directory, and using Python to organize my
 66messages into the newly-created sub-folders.
 67
 68** Sent Messages
 69
 70I started by organizing my Sent messages. This required checking each file for
 71the =From= header and moving them to the Sent folder.
 72
 73#+begin_src shell
 74cd ~/.maildir/migadu/Archive/cur
 75nano _sent.py
 76#+end_src
 77
 78#+begin_src python
 79# _sent.py
 80import os
 81import glob
 82import shutil
 83
 84# Loop through all files in the current folder
 85for file in glob.glob("*.eml"):
 86	# Create boolean to check if we should move the file
 87	move = False
 88
 89	# Open the current file
 90		f = open(file, 'r')
 91
 92	# For each line in file, find the From header
 93	for line in f:
 94		if line.startswith("From:"):
 95			# If we find ourself, mark the message for move
 96			if "user@example.com" in line:
 97				move = True
 98
 99	# Close the file
100	f.close()
101
102	# Move the file, if marked for move
103	if move == True:
104		filepath = os.path.join("/Users/YOUR_USERNAME/.maildir/migadu/Archive/cur/", file)
105		new_filepath = os.path.join("/Users/YOUR_USERNAME/.maildir/migadu/Sent/cur/", file)
106		shutil.move(filepath, new_filepath)
107#+end_src
108
109#+begin_src python
110python3 _sent.py
111#+end_src
112
113The only downside to my current approach is that it was the quick and dirty
114option, so I re-ran it while editing the =user@example.com= string for each
115email I wanted to move. If I had wanted to create a more well-defined solution,
116I would have created an array of addresses to check for and have the =if=
117statement check against that array.
118
119Regardless, I was able to run this with the addresses I wanted to move to the
120Sent folder and was soon finished.
121
122** Archive Sub-Folders
123
124Next, I needed to move the remaining ~3000 messages from the Archive folder into
125dated sub-folders, organized as such:
126
127- Archive/2016
128- ...
129- Archive/2025
130
131
132To do this, I followed a similar approach as the method above but check for the
133=Date= header instead of the =From= header.
134
135#+begin_src shell
136cd ~/.maildir/migadu/Archive/cur
137nano _archive.py
138#+end_src
139
140This approach requires finding the =X-Pm-Date= header and splitting it by the
141spaces contained within. Once split into a list, we must select the fourth
142element, as that contains the year which will match the directory we should move
143it to.
144
145For example, the header =X-Pm-Date: Fri, 07 Feb 2025 16:12:08 +0000= will be
146split into a list as such:
147
148#+begin_src python
149[
150    'X-Pm-Date:', # 0
151    'Fri,',       # 1
152    '07',         # 2
153    'Feb',        # 3
154    '2025',       # 4
155    '16:12:08',   # 5
156    '+0000'       # 6
157]
158#+end_src
159
160From this list, we select the fourth element (=2025=) and use that to build the
161destination path.
162
163#+begin_src python
164# _archive.py
165import os
166import glob
167import shutil
168
169# Loop through all files in the sub-folders under Archive
170for file in glob.glob("*.eml"):
171	# Create boolean to check if we should move the file
172	move = False
173
174	# Open the current file
175	f = open(file, 'r')
176
177	# For each line in file, find the X-Pm-Date header
178	for line in f:
179		if line.startswith("X-Pm-Date"):
180		# Split the line into a list by spaces;
181		# Then select the item that contains the year
182			year = line.split(" ")[4]
183			move = True
184
185	# Close the file
186	f.close()
187
188	# Move the file, if marked for move
189	if move == True:
190		filepath = os.path.join("/Users/YOUR_USERNAME/.maildir/migadu/Archive/cur/", file)
191		new_filepath = os.path.join(f"/Users/YOUR_USERNAME/.maildir/migadu/Archive/{year}/cur/", file)
192		shutil.move(filepath, new_filepath)
193#+end_src
194
195#+begin_src python
196python3 _archive.py
197#+end_src
198
199At this point, we've now moved all Sent messages to the Sent box and organized
200all messages under the Archive folder into their correct sub-folders.
201
202If you exported other files, such as files from your Inbox, Trash, etc., you
203could follow a similar approach and determine the best header or attribute to
204identify them for further organization.
205
206** Synchronize the Results
207
208Before synchronizing the files in their new locations, I needed to remove the
209characters at the end of the file name since =mu= appends IDs to the end of file
210names.
211
212#+begin_src shell
213cd ~/.maildir/migadu/Archive
214nano _sync_prep.py
215#+end_src
216
217This script prepares the =Archive= sub-folders for synchronization, but the same
218concept applies to the Sent folder, except you'd replace =*/cur/*= with =*= if
219this script were inside the =Sent/cur= directory.
220
221#+begin_src python
222import glob
223import shutil
224
225# Loop through all files in the sub-folders under Archive
226for file in glob.glob("*/cur/*"):
227	# Remove the characters at the end of the file name created by =mu=
228	new_file = file.split(",U=",1)[0]
229
230	# Move the file to the new file name
231	shutil.move(file, new_file)
232#+end_src
233
234#+begin_src shell
235python3 _sync_prep.py
236#+end_src
237
238Finally, we can synchronize the results.
239
240#+begin_src shell
241mbsync -aV
242#+end_src
243
244* Removing Duplicates
245
246My only remaining issue at the time of writing is identifying and removing
247duplicate messages. I have toyed with simple Python and command-line solutions
248to identify duplicate files, but could not get them to effectively define all
249the duplicates found in any specific directory.
250
251I've even tried using the [[https://github.com/pkolaczk/fclones][fclones]] utility, to no avail. It seems that something
252in the Proton export, my manual Thunderbird method attempt, or possible sync
253issues between Thunderbird -> Migadu <-> mu caused duplicates where content
254within the message has been modified.
255
256Although I now seem to be wasting space and in need of a deduplication tool, I
257have all of my messages migrated to my new service.