Probably the world's fastest DBX extractor — mathematically and hardware-optimized algorithm.
1 GB database extracted in under a second, while competing tools take several minutes.

DBXExtractor otwiera pliki .dbx — wewnętrzny format archiwów skrzynki pocztowej Outlook Express (OE 4–6) — i zapisuje każdą przechowaną wiadomość jako osobny plik .eml zgodny z RFC 2822. Pliki EML otwiera bezpośrednio Microsoft Outlook, Thunderbird i każdy inny klient obsługujący standard MIME.
Program działa w dwóch trybach uruchomienia:
| Tryb | Uruchomienie | Zastosowanie |
|---|---|---|
| GUI | podwójne kliknięcie, bez argumentów | interaktywna praca z paskiem postępu i logiem |
| CLI | z argumentami wiersza poleceń | skrypty, automatyzacja, integracja z PowerShell |
Oba tryby korzystają z tego samego silnika ekstrakcji (Extraction.cpp + dbxread.c); GUI to warstwa prezentacji na wierzchu wspólnego rdzenia.
Główne okno (620 × 580 px, WS_OVERLAPPEDWINDOW) zawiera:
| Kontrolka | Opis |
|---|---|
| DBX Source | ścieżka do folderu z plikami .dbx lub do pojedynczego pliku .dbx |
| Output Folder | folder docelowy; każdy plik DBX otrzymuje własny podfolder |
| Browse... | otwiera SHBrowseForFolderW (folder) lub GetOpenFileNameW (plik) |
| Extraction Options | cztery checkboxy omówione niżej |
| Verbosity | poziom szczegółowości logu (combobox, 6 poziomów) |
| Extract | przycisk owner-draw uruchamiający wątek ekstrakcji |
| Progress bar | PBS_SMOOTH, niewidoczny do czasu startu ekstrakcji |
| Output Log | wieloliniowy ES_READONLY, czcionka Consolas |
| Status bar | SBARS_SIZEGRIP, tekst „Ready" / „Extracting… N%" / „Done." |
Każdy checkbox jest zsynchronizowany z odpowiadającą mu pozycją menu Options — zmiana jednego automatycznie aktualizuje drugi.
| Opcja | Flaga | Zachowanie |
|---|---|---|
| Recovery Mode | options.recover |
zamiast normalnej ekstrakcji uruchamia ścieżkę _recover(), która szuka fragmentów wiadomości usuniętych lub poza normalnym indeksem |
| Safe Mode | options.safe_mode |
generuje nazwy plików EML bezpieczne dla każdej lokalizacji (tylko znaki ASCII + podkreślenie), przydatne gdy docelowy system plików nie obsługuje Unicode |
| Delete Deleted | options.delete_deleted |
usuwa z folderu docelowego pliki EML których nie ma już w DBX; domyślnie (odznaczone) takie pliki są przenoszone do podfolderu deleted zamiast kasowania |
| Ignore Empty | options.ignore0 |
pomija wiadomości z offset == 0, które oznaczają zazwyczaj uszkodzone lub nieistniejące wpisy indeksu |
Postęp jest raportowany atomowo: InterlockedIncrement na liczniku done w każdym wątku roboczym, co 32 wiadomości lub po zakończeniu całości — PostMessage(WM_PROGRESS_UPDATE, pct). Wątek UI aktualizuje pasek i status bar bez żadnej synchronizacji blokującej.
Program używa niedokumentowanego API uxtheme.dll (zestaw undocumented exports przez wskaźnik indeksowy) do włączenia ciemnego motywu na Windows 10+. Na Windows 11 (build 22000+) aktywowany jest efekt MICA (DWM_SYSTEMBACKDROP_TYPE = DWMSBT_MAINWINDOW), który nadaje tłu przezroczystą teksturę systemu.
Paleta Palette zawiera: bg, surface, input, text, textDim, accent, accentHover, accentPress, border. Zmiana motywu systemu w czasie działania programu (WM_SETTINGCHANGE) przebudowuje paletę i odświeża okno bez restartu.
Przyciski Extract i Browse... są rysowane jako owner-draw (BS_OWNERDRAW) z zaokrąglonymi narożnikami i pełną obsługą stanów Hover/Press. Ramka grupy Extraction Options rysowana jest ręcznie w WM_PAINT rodzica (zamiast BS_GROUPBOX child window), co eliminuje artefakty ghost-border przy live resize z WS_CLIPCHILDREN.
Resize okna używa BeginDeferWindowPos / EndDeferWindowPos z WM_SETREDRAW FALSE/TRUE + RDW_ALLCHILDREN | RDW_UPDATENOW — eliminuje flicker i ghost pixels przy płynnym przeciąganiu krawędzi.
Gdy wWinMain wykryje argumenty wiersza poleceń (argc > 1), wywołuje RunCLI() zamiast RunGUI().
DBXExtractor.exe [OPTIONS] <DBX-FOLDER | DBX-FILE> [OUTPUT-FOLDER]
Jeśli OUTPUT-FOLDER jest pominięty, jako folder wyjściowy używany jest bieżący katalog roboczy (.).
| Opcja | Opis |
|---|---|
-h, --help |
wyświetl pomoc i zakończ |
-V, --version |
wyświetl wersję i zakończ |
-v N, --verbosity N |
poziom szczegółowości 0–5 (domyślnie: 3 = Info) |
-r, --recover |
tryb odzyskiwania |
-s, --safe-mode |
bezpieczne nazwy plików (tylko ASCII) |
-D, --delete |
usuń pliki EML których nie ma w DBX (domyślnie: przenieś do deleted) |
-i, --ignore0 |
ignoruj wiadomości z offset 0 |
-d, --debug |
komunikaty debugowania |
-c N, --codepage N |
strona kodowa metadanych DBX (nadawca, temat); domyślnie: CP_ACP |
# Wyodrębnij wszystkie wiadomości z Inbox.dbx do bieżącego folderu
DBXExtractor.exe "C:\OE\Inbox.dbx"
# Wyodrębnij Sent Items do konkretnego folderu
DBXExtractor.exe "C:\OE\Sent Items.dbx" "C:\Exported\Sent"
# Wyodrębnij cały folder skrzynki OE
DBXExtractor.exe "C:\OE" "C:\Exported"
# Polska skrzynka na angielskim Windows (CP_ACP != 1250)
DBXExtractor.exe -c 1250 "C:\OE" "C:\Exported"
# Wildcard: tylko pliki z rozszerzeniem .bak które są prawdziwymi plikami DBX
DBXExtractor.exe "C:\OE\*.bak" "C:\Exported"
Ponieważ DBXExtractor.exe jest aplikacją GUI (/SUBSYSTEM:WINDOWS), nie ma konsoli z urzędu. SetupConsole() dołącza się do konsoli procesu nadrzędnego (AttachConsole(ATTACH_PARENT_PROCESS)), a jeśli jej brak — tworzy nową (AllocConsole()). Następnie strumienie stdout/stderr są przekierowane przez _open_osfhandle + freopen_s("CONOUT$").
SetConsoleOutputCP(CP_UTF8) ustawia UTF-8 dla całego wyjścia — ścieżki z polskimi znakami są wyświetlane poprawnie niezależnie od systemowej strony kodowej.
SendEnterToConsole() wysyła VK_RETURN do bufora wejściowego konsoli, co automatycznie odpowiada na ewentualny prompt „Press any key…" po wyświetleniu helpu, gdy nie było konsoli rodzica.
Każdy plik DBX zaczyna się od 16-bajtowej sygnatury weryfikowanej przez _dbx_has_signature():
/* Outlook Express 5/6 email store */
sig[0] == 0xFE12ADCF &&
(sig[1] == 0x6F74FDC5 || sig[1] == 0x6F74FDC6) &&
sig[2] == 0x11D1E366 &&
sig[3] == 0xC0004E9A
/* Outlook Express 4 (stary format JMF) */
sig[0] == 0x36464D4A && sig[1] == 0x00010003
Sprawdzenie sygnatury jest wykonywane przed dodaniem pliku do listy przetwarzania. Dzięki temu wildcardy (np. *.bak) działają poprawnie — niebędące DBX pliki są po cichu pomijane.
| Typ | Stała | Opis |
|---|---|---|
DBX_TYPE_EMAIL |
główny typ | folder wiadomości (Inbox, Sent, itp.) — jedyny przetwarzany normalnie |
DBX_TYPE_FOLDER |
folder meta | plik indeksu folderów (Folders.dbx) — pomijany |
DBX_TYPE_OE4 |
legacy | stary format OE4 |
DBX_TYPE_UNKNOWN |
nieznany | pominięty |
Plik DBX jest plikiem binarnym z listą indeksów (dbx_info_t) wskazujących na offsety fragmentów wiadomości w obrębie pliku. Każdy indeks dbx_info_t przechowuje metadane wiadomości:
| Pole | Typ | Opis |
|---|---|---|
filename |
char * |
nazwa pliku wyjściowego EML (generowana przez parser) |
offset |
int |
offset bajtu początku danych wiadomości w pliku DBX |
message_size |
uint |
rozmiar wiadomości w bajtach wg indeksu |
send_create_time |
filetime_t |
czas wysłania (FILETIME Windows) |
receive_create_time |
filetime_t |
czas odebrania |
subject |
char * |
temat (UTF-8 po dekodowaniu) |
sender_name |
char * |
nazwa nadawcy |
sender_address |
char * |
adres e-mail nadawcy |
flags |
uint |
flagi statusu (przeczytana, odpowiedziana, itp.) |
valid |
dbx_mask_t |
bitmapa pól zwalidowanych przez parser |
Pole valid zawiera OR flag DBX_MASK_* — parser ustawia odpowiedni bit tylko dla pól, które udało mu się poprawnie odczytać z pliku.
Cały plik DBX jest mapowany do pamięci jednym wywołaniem MapViewOfFile przy dbx_open(). Odczyty to czyste operacje na wskaźnikach bez fseek/fread — system operacyjny ładuje strony na żądanie (demand paging). Szczegóły w sekcji Memory-mapped I/O.
Plik EML to surowy RFC 2822 — dokładnie ta sama sekwencja bajtów, która była przechowywana wewnątrz DBX. DBXExtractor nie konwertuje ani nie modyfikuje treści wiadomości; zapisuje ją wprost do pliku bez żadnych transformacji.
Microsoft Outlook otwiera pliki .eml bezpośrednio przez dwuklik lub przez File → Open. Thunderbird, Windows Mail i każdy klient zgodny z RFC 2822 również.
Parser generuje nazwę pliku EML na podstawie metadanych wiadomości z indeksu DBX (temat, timestamp, nadawca). W trybie --safe-mode nazwa zawiera wyłącznie znaki ASCII + podkreślenie, co zapewnia kompatybilność z systemami plików FAT32 i starszymi narzędziami.
Pliki EML już istniejące w folderze docelowym są porównywane po rozmiarze z rozmiarem z indeksu DBX. Plik jest nadpisywany tylko jeśli rozmiar się różni (DBX_EXTRACT_MAYBE). Nowe wiadomości są zapisywane bezwarunkowo (DBX_EXTRACT_FORCE). Pliki EML bez odpowiadającej im wiadomości w DBX trafiają do podfolderu deleted (lub są kasowane przy --delete).
Rdzeń parsowania DBX (dbxread.c, dbxsys.c, dbxprogress.c, emlread.c) jest napisany w czystym C z blokami #ifdef __cplusplus extern "C" — zachowuje kompatybilność z oryginalnym kodem undbx/DbxConv i umożliwia kompilację zarówno jako C jak i C++. Warstwa GUI i CLI jest w C++23.
Funkcja _undbx() w Extraction.cpp jest głównym punktem wejścia dla jednego pliku DBX.
Parser porównuje posortowaną listę wiadomości z indeksu DBX z posortowaną listą istniejących plików EML w folderze docelowym (merge join dwóch posortowanych list). Wynikiem jest przypisanie jednej z trzech decyzji do każdego dbx_info_t.extract:
| Decyzja | Znaczenie |
|---|---|
DBX_EXTRACT_FORCE |
nowa wiadomość — zapisz bezwarunkowo |
DBX_EXTRACT_MAYBE |
wiadomość istnieje — zapisz jeśli rozmiar się zmienił |
DBX_EXTRACT_IGNORE |
wiadomość z offset 0 i --ignore0 aktywny |
Pliki EML bez odpowiednika w indeksu DBX są obsługiwane przez sys_move (do deleted/) lub sys_delete zgodnie z flagą delete_deleted.
Przed fazą 2 lista dbx->info jest sortowana po offset (rosnąco) przez qsort. Sortowanie po offsetie pliku gwarantuje sekwencyjny dostęp do zmapowanej pamięci wewnątrz każdego wątku — strony DBX są odczytywane w kolejności, co minimalizuje page faults i maksymalizuje efektywność cache TLB.
Szczegóły puli wątków w sekcji Równoległy zapis plików EML.
Funkcja _run_parallel_save() dzieli tablicę wiadomości na N ciągłych plasterków i uruchamia N wątków Win32, gdzie N = liczba procesorów logicznych (GetSystemInfo().dwNumberOfProcessors), ograniczona do 64 i do liczby wiadomości.
int nthreads = (int)si.dwNumberOfProcessors;
if (nthreads > 64) nthreads = 64;
if (nthreads > dbx->message_count) nthreads = dbx->message_count;
Każdy wątek przetwarza wyłącznie swój plasterek — nie ma współdzielonego stanu mutowalnego. dbx->_base (zmapowany widok pliku) jest const i tylko do odczytu. Każdy plik EML ma unikalną ścieżkę — brak wyścigów na plikach wyjściowych.
typedef struct {
volatile LONG done; /* przetworzone wiadomości (dla % postępu) */
volatile LONG saved; /* zapisane pliki EML */
volatile LONG errors; /* błędy zapisu */
int total; /* dbx->message_count (stała) */
} ParallelCtrs;
InterlockedIncrement gwarantuje atomowość inkrementacji bez mutexów. PostMessage(WM_PROGRESS_UPDATE) co 32 wiadomości lub przy zakończeniu — wątek UI aktualizuje pasek bez blokowania wątków roboczych.
Jeśli calloc na tablicę uchwytów wątków zawiedzie (OOM), ekstrakcja odpada na fallback seryjny — pętla po wszystkich wiadomościach w głównym wątku extraction, bez utraty danych.
sys_chdir w wątkachIstotna zmiana względem oryginalnego undbx.c: funkcje _save_message, _set_message_time, _set_message_filetime i _filesize_abs budują absolutne ścieżki przez _build_path(dir, filename) zamiast sys_chdir(dir) + fopen(filename). sys_chdir jest operacją globalną dla procesu (zmienia CWD), co przy wywołaniu z wielu wątków jednocześnie powoduje wyścigi. Wywołanie _wfopen z absolutną ścieżką jest thread-safe.
Gdy options.recover == 1, zamiast _extract() wywołana jest _recover(). Tryb odzyskiwania skanuje zawartość pliku DBX w poszukiwaniu łańcuchów fragmentów (dbx_chains_t) — również tych oznaczonych jako usunięte — i próbuje odtworzyć z nich kompletne wiadomości.
Każdy łańcuch (dbx_chains_t) zawiera listę obiektów dbx_fragment_t z polami offset, offset_next, offset_prev, size. Parser scala fragmenty w kolejności łańcucha i wywołuje dbx_recover_message(), która zwraca surowy bufor wiadomości wraz z timestampem i wygenerowaną nazwą pliku.
Wiadomości odzyskane z łańcuchów usuniętych (scan[i].deleted == 1) trafiają do podfolderu deleted wewnątrz folderu wyjściowego. Wiadomości aktywne trafiają bezpośrednio do głównego folderu EML.
Outlook Express zapisywał metadane wiadomości (temat, nadawca) w systemowej stronie kodowej ANSI maszyny, która utworzyła plik DBX. Polska wersja OE używała CP 1250. Otwarcie takiego pliku na angielskim Windows (CP_ACP = 1252) daje przekłamania znaków.
Funkcja _dbx_detect_codepage() (wywoływana na początku dbx_open()) skanuje pierwsze DBX_DETECT_SCAN_BYTES bajtów zmapowanego pliku w poszukiwaniu nagłówków RFC 2047 w postaci =?charset?...?=. Rozpoznana nazwa zestawu znaków (np. iso-8859-2, windows-1250) jest mapowana do numeru strony kodowej Windows przez sys_charset_to_codepage(). Wynik jest zapisywany w dbx->detected_codepage.
=?iso-8859-2?Q?Za=BF=F3=B3cenie?= → CP 28592
=?windows-1250?B?...?= → CP 1250
options->metadata_codepage (--codepage N, gdy użytkownik podał jawnie)
↓ jeśli 0
dbx->detected_codepage (autodetekcja z RFC2047 w zawartości pliku)
↓ jeśli 0
CP_ACP (systemowa ANSI, fallback)
Wszystkie ciągi metadanych odczytane z DBX są konwertowane do UTF-8 przez _cp_to_utf8():
MultiByteToWideChar(codepage, 0, bytes, len, wide, wlen) → wide
WideCharToMultiByte(CP_UTF8, 0, wide, -1, utf8, ulen, …) → utf8
Analogicznie emlread.c dekoduje nagłówki RFC 2047 (=?charset?Q?...?= i =?charset?B?...?=) w samej treści EML — quoted-printable i Base64 — przez _eml_parse_encoded_word() z konwersją do UTF-8.
Pliki DBX mogą być duże (do ~2 GB — ponad 2 GB jest traktowane jako uszkodzenie z ostrzeżeniem). Oryginalny undbx.c używał fseek/fread. Implementacja DBXExtractor zastępuje to mapowaniem pamięci:
dbx->_hFile = CreateFileW(wpath, GENERIC_READ, FILE_SHARE_READ, …);
dbx->_hMapping = CreateFileMappingW(dbx->_hFile, NULL, PAGE_READONLY, 0, 0, NULL);
dbx->_base = MapViewOfFile(dbx->_hMapping, FILE_MAP_READ, 0, 0, 0);
Wszystkie odczyty to operacje na wskaźnikach (memcpy ze sprawdzeniem granic) przez inline helpery _mr_i32, _mr_i16, _mr_i64, _mr_buf, _mr_u8. Bounds checking gwarantuje, że odczyt poza końcem pliku zwraca 0 / pustą pamięć zamiast segfaultu.
Korzyści dla wydajności:
offset dostęp do fragmentów w każdym wątku jest sekwencyjny — prefetcher CPU i TLB pracują optymalnie.FILE_SHARE_READ — inne procesy mogą czytać DBX jednocześnie.Przy dbx_close() mapowanie jest zwalniane przez UnmapViewOfFile + CloseHandle(_hMapping) + CloseHandle(_hFile).
Na systemach nie-Windows (Linux/macOS, ścieżka #else w dbxread.c) plik jest wczytywany do bufora przez malloc + fread — semantyka kodu pozostaje identyczna.
Wszystkie operacje na plikach i katalogach w dbxsys.c (Windows path) przechodzą przez wewnętrzną konwersję UTF-8 → UTF-16 przed przekazaniem do Win32 API:
static wchar_t *utf8_to_wide(const char *utf8) {
int n = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0);
wchar_t *ws = malloc(n * sizeof(wchar_t));
MultiByteToWideChar(CP_UTF8, 0, utf8, -1, ws, n);
return ws;
}
Funkcje sys, które korzystają z tej konwersji:
| Funkcja | Win32 API |
|---|---|
sys_fopen(path, mode) |
_wfopen(wide, wmode) |
sys_glob(parent, pattern, n) |
FindFirstFileW / FindNextFileW |
sys_mkdir(parent, dir) |
CreateDirectoryW |
sys_chdir(dir) |
SetCurrentDirectoryW |
sys_delete(parent, filename) |
DeleteFileW |
sys_move(parent, file, dest) |
MoveFileW |
sys_set_filetime(path, ft) |
CreateFileW + SetFileTime |
Wyjście do konsoli w RunCLI(): SetConsoleOutputCP(CP_UTF8) + argumenty konwertowane WideCharToMultiByte(CP_UTF8) — ścieżki z polskimi znakami wyświetlają się poprawnie w Windows Terminal i PowerShell.
DBX przechowuje listę wiadomości w strukturze B-drzewa. Główny wskaźnik do korzenia drzewa indeksów leży pod stałym offsetem INDEX_POINTER (0xE4) w nagłówku pliku. Funkcja _dbx_read_indexes() rekurencyjnie odczytuje węzły drzewa przez _dbx_read_index().
Każdy węzeł indeksu zawiera tablicę wskaźników do rekordów dbx_info_t. Pojemność tablicy dbx->info jest zarządzana dynamicznie przez realloc z krokiem ptr_count per węzeł — dbx->message_count rośnie w miarę przechodzenia drzewa.
Każdy rekord dbx_info_t w pliku zawiera liczbę pól (count) zakodowaną w bitach 16–23 słowa na index+8. Kolejne pola leżą od index+12 w krokach po 4 bajty. Każde pole jest zakodowaną parą (type, value):
wartość 32-bit = (type:8)(value:24)
bit 7 type = 1 → value jest wartością bezpośrednią (inline storage)
bit 7 type = 0 → value jest offsetem do bloku danych extra
Numery typów rozpoznawane przez parser:
| Typ (hex) | Pole dbx_info_t |
Format |
|---|---|---|
0x00 |
message_index |
int |
0x01 |
flags |
int (bitmapa statusu OE) |
0x02 |
send_create_time |
FILETIME (8B) |
0x03 |
body_lines |
int |
0x04 |
message_address |
int |
0x05 |
original_subject |
string |
0x06 |
save_time |
FILETIME |
0x07 |
message_id |
string |
0x08 |
subject |
string |
0x09 |
sender_address_and_name |
string |
0x0A |
message_id_replied_to |
string |
0x0C |
server |
string |
0x0D |
sender_name |
string |
0x0E |
sender_address |
string |
0x10 |
message_priority |
int |
0x11 |
message_size |
int |
0x12 |
receive_create_time |
FILETIME |
0x13 |
receiver_name |
string |
0x14 |
receiver_address |
string |
0x1A |
account_name |
string |
0x1B |
account_registry_key |
string |
Pole valid jest bitmapą flag DBX_MASK_* — parser ustawia bit tylko dla pól, które faktycznie odczytał. Brak bitu DBX_MASK_MSGSIZE oznacza, że _maybe_save_message nie może polegać na porównaniu rozmiaru i musi zawsze wykonać odczyt blokowy.
Dane wiadomości przechowywane są w pliku DBX jako łańcuch bloków. Każdy blok ma:
offset bloku + 8 → int16: rozmiar payload (bsz, max 0x200 = 512 B)
offset bloku + 12 → int32: offset następnego bloku (0 = koniec łańcucha)
offset bloku + 16 → bsz bajtów payload
dbx_message() przechodzi łańcuch dwukrotnie (two-pass):
bsz w całym łańcuchu → total; jednorazowy malloc(total+1)._mr_buf (bounds-checked memcpy z widoku mmap).Blok z bsz <= 0 lub bsz > 0x200 jest sygnałem uszkodzenia — emitowane ostrzeżenie i przerwanie łańcucha. Wynikowy bufor kończy się \0 i jest zwracany do _save_message → fwrite.
Dwa tryby generowania nazwy w _dbx_set_filename():
--safe-mode) — wyłącznie %08X.eml gdzie hex to offset wiadomości w pliku DBX; gwarantuje unikalność i pełną kompatybilność ASCII.emlread.c implementuje parser nagłówków RFC 2822 potrzebnych do wyekstrahowania metadanych wyświetlanych w logu i potrzebnych do nazewnictwa pliku EML (subject, from, to, timestamp). Nie jest pełnym parserem MIME — obsługuje tylko podzbiór wystarczający do tych potrzeb.
Nagłówki z kodowaniem RFC 2047 mają postać:
=?charset?encoding?encoded_text?=
gdzie encoding to Q (Quoted-Printable) lub B (Base64). Parser _eml_parse_encoded_word() rozpoznaje oba warianty i konwertuje wynik do UTF-8 przez _cp_to_utf8_eml(bytes, len, codepage):
=?iso-8859-2?Q?Za=BF=F3=B3cenie?=
↓ _eml_decode_qp → raw bytes w CP 28592
↓ _cp_to_utf8_eml(CP 28592)
→ "Zażółcenie" (UTF-8)
=?windows-1250?B?emHFvMOzxYNjaWU=?=
↓ _eml_decode_base64 → raw bytes w CP 1250
↓ _cp_to_utf8_eml(CP 1250)
→ "zażółcie" (UTF-8)
Mapowanie nazwy zestawu znaków na numer strony kodowej Windows odbywa się przez sys_charset_to_codepage() z dbxsys.c — obsługuje m.in. iso-8859-1, iso-8859-2, windows-1250, windows-1252, koi8-r, utf-8 (dla UTF-8 zwraca 0 — brak konwersji).
emlread.c używa prostego zestawu pomocników _eml_str_append_n, _eml_str_append_char, _eml_str_append_range, _eml_str_free — minimalistyczny dynamiczny bufor stringów bez zewnętrznych zależności.
Nagłówek Date: w RFC 2822 ma postać:
Date: Mon, 21 Apr 2003 14:30:00 +0200
Parser _eml_parse_date() rozkłada go na strukturę tm i wywołuje mktime(), zwracając time_t UTC. Timestamp jest używany do ustawienia czasu modyfikacji pliku EML przez sys_set_filetime() — po ekstrakcji plik .eml ma datę odpowiadającą dacie odebrania wiadomości, nie dacie ekstrakcji.
dbxprogress.c implementuje hierarchiczny system komunikatów z paskami postępu. Kluczowe pojęcia:
dbx_progress_push() / dbx_progress_pop() utrzymują stos dbx_progress_bar_t. Każde wywołanie push tworzy nowy pasek z podaną maksymalną liczbą kroków; pop zamyka pasek i emituje podsumowanie. Paskowanie zagłębione (np. całość pliku → każda wiadomość) daje czytelne wcięcia w logu.
static dbx_output_fn_t g_output_fn = NULL;
void dbx_progress_set_output(dbx_output_fn_t fn);
Gdy g_output_fn jest NULL, logi trafiają na stdout/stderr (tryb CLI). Gdy jest ustawiony na GuiProgressCallback, logi trafiają przez PostMessageA(WM_APPEND_LOG) do wątku UI — nigdy bezpośrednio, zawsze asynchronicznie.
Linie postępu emitowane do callbacku zaczynają się od \nN.N% [STATUS]:
\n 42.0% [OK] 00001A2B.eml
\n 42.0% ← bare ping (tylko aktualizacja paska, bez logu)
GuiProgressCallback parsuje prefix — jeśli po % nie ma nic, komunikat jest czystym pingiem i nie trafia do loga. Jeśli po % jest tekst, jest dołączany do g_hLogEdit.
Rendering.cpp definiuje makro S(hwnd, dp) które skaluje logiczne „punkty dialogowe" do pikseli fizycznych z uwzględnieniem DPI okna:
#define S(hwnd, dp) MulDiv(dp, GetDpiForWindow(hwnd), 96)
Wszystkie pozycje i rozmiary kontrolek są podawane w jednostkach 96-DPI i automatycznie skalowane na monitorach HiDPI. Przy WM_SIZE LayoutControls() przelicza pozycje na nowo — okno reaguje poprawnie na przeciąganie między monitorami z różnym DPI.
LayoutControls() używa BeginDeferWindowPos / DeferWindowPos / EndDeferWindowPos zamiast pętli SetWindowPos. Wszystkie kontrolki są przesuwane w jednej transakcji DWM:
HDWP hdwp = BeginDeferWindowPos(8);
hdwp = DeferWindowPos(hdwp, hEdit, NULL, x, y, w, h, F);
// ... dla każdej kontrolki
EndDeferWindowPos(hdwp);
Połączone z WM_SETREDRAW FALSE/TRUE i RDW_ALLCHILDREN | RDW_UPDATENOW eliminuje flicker i ghost pixels przy live resize — nawet przy szybkim przeciąganiu krawędzi okna.
Combobox IDC_COMBO_VERBOSITY jest wypełniany 6 etykietami (Quiet … Debug) i inicjalizowany na indeks 3 (Info (default)). Przy starcie ekstrakcji StartExtraction() odczytuje wybraną pozycję i ustawia options.verbosity = (dbx_verbosity_t)sel.
ShowHelpDialog() tworzy pseudo-modalne okno (WS_POPUP | WS_CAPTION | WS_SYSMENU) z czcionką Consolas do wyrównania kolumn. Modalność jest emulowana własną pętlą GetMessageW:
EnableWindow(parent, FALSE);
MSG msg;
while (IsWindow(hdlg) && GetMessageW(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
EnableWindow(parent, TRUE);
IsDialogMessageW celowo nie jest używany — na tablecach z gładzikowym WM_POINTERWHEEL / WM_GESTURE błędnie interpretuje te zdarzenia jako VK_ESCAPE i zamykałby okno przy pierwszym geście. Zamiast tego WM_KEYDOWN obsługuje VK_ESCAPE i VK_RETURN jawnie w HelpWndProc.
Okno jest centrowane nad rodzicem przez GetWindowRect + SetWindowPos(SWP_NOSIZE | SWP_NOZORDER).
AppGlobals.h deklaruje globalne zmienne widoczne we wszystkich modułach C++:
| Zmienna | Typ | Opis |
|---|---|---|
g_hMainWnd |
HWND |
uchwyt głównego okna — używany przez wątki do PostMessage |
g_hProgress |
HWND |
pasek postępu PBS_SMOOTH |
g_hLogEdit |
HWND |
wieloliniowy log ES_READONLY |
g_hExtractBtn |
HWND |
przycisk Extract (owner-draw) |
g_hStatusBar |
HWND |
pasek stanu z uchwytem resize |
g_hFontUI |
HFONT |
Segoe UI 10pt — kontrolki UI |
g_hFontMono |
HFONT |
Consolas 9pt — log i dialog pomocy |
g_hbrBg |
HBRUSH |
pędzel tła okna (zgodny z paletą) |
g_hbrInput |
HBRUSH |
pędzel tła pól edycji |
g_isDark |
BOOL |
aktualny tryb ciemny/jasny |
g_extracting |
BOOL |
flaga blokady UI podczas ekstrakcji |
g_pal |
Palette |
aktywna paleta kolorów |
| Cecha C++23 | Gdzie użyta |
|---|---|
Lambda z przechwyceniem [&] (C++11+) |
CreateControls: lokalne Btn, Lbl, Edit, Chk lambdy tworzące kontrolki |
Lambda ToggleMenuCheck |
WindowProc WM_COMMAND: synchronizacja menu ↔ checkbox bez powielania kodu |
| Designated initializers | inicjalizacja CliArgs a = {}, dbx_options_t options = {} |
[[nodiscard]] |
_undbx(), _get_files(), dbx_message(), dbx_open() |
std::atomic-like volatile LONG + InterlockedIncrement |
ParallelCtrs — thread-safe liczniki bez mutexów |
| Range-based for | for (auto h : ctrls) SetF(h, g_hFontUI) w CreateControls |
auto lambda capture |
SetF lambda w CreateControls |
constexpr / inline constexpr |
stałe DBX_VERSION, APP_TITLE, APP_CLASS |
/std:c++latest |
aktywny standard C++23 z __cplusplus = 202302L |
bool literals |
CliArgs struct pola help, version, recover, safe_mode, debug, error |
W trybie odzyskiwania dbxread.c wykonuje liniowy skan całego zmapowanego pliku w poszukiwaniu bloków-fragmentów, które mogą należeć do usuniętych lub sierocych wiadomości. Algorytm jest oparty na przesuwającym się oknie 8-słów (sliding window).
Każdy blok danych w DBX ma 20-bajtowy nagłówek:
+0 (4B) offset tego bloku w pliku
+4 (4B) junk / nieużywane
+8 (2B) rozmiar payload bsz (max 0x200)
+10 (2B) nieużywane
+12 (4B) offset następnego bloku (0 = koniec)
+16 (4B) offset poprzedniego bloku
+20 … payload bsz bajtów
Skaner przechodzi przez plik z krokiem 4 bajty, utrzymując okno ostatnich 8 odczytanych int32. Na każdej pozycji sprawdza, czy okno pasuje do sygnatury nagłówka fragmentu:
int message_fragment_found =
header[header_start] /* offset bieżący */
&& header[(header_start + 2) & 7] /* rozmiar > 0 */
&& header[(header_start + 2) & 7] <= 0x200; /* rozmiar ≤ 512 */
int deleted_fragment_found =
header[header_start]
&& header[(header_start + 2) & 7]
&& header[(header_start + 2) & 7] <= 0x200
&& header[(header_start + 4) & 7] == header[header_start];
Dodatkowy warunek dla usuniętych: offset_prev == offset_bieżący — wykrywa fragmenty z „okrężnym" wskaźnikiem wstecznym, charakterystycznym dla rekordów usuniętych przez OE.
Dla każdego rozpoznanego fragmentu _dbx_get_scan_chains() sprawdza, czy łączy się z poprzednio znalezionym fragmentem (przez offset_next lub offset_prev). Jeśli tak — jest dołączany do istniejącego łańcucha (chains->count-- — zmniejszamy liczbę niezależnych łańcuchów). Jeśli nie — tworzy nowy łańcuch.
Tablice fragmentów i łańcuchów są alokowane z krokiem 4096 elementów przez realloc — unikają częstych realokacji przy dużych uszkodzonych plikach.
dbx_recover_message() dla danego łańcucha (chain_index, msg_number) scala payload wszystkich fragmentów w kolejności łańcucha w jeden bufor. Następnie wywołuje eml_parse() na odzyskanym buforze, żeby wyekstrahować subject, to, timestamp do sformowania nazwy pliku EML.
Timestamp z eml_parse() jest konwertowany do time_t i przekazywany do _set_message_time() — plik odzyskanej wiadomości otrzymuje datę zbliżoną do oryginalnej, jeśli nagłówek Date: przetrwał w odzyskanym fragmencie.
Funkcja _get_files() obsługuje trzy scenariusze wejścia:
DBXExtractor.exe "C:\OE"
sys_glob(dir, "*.dbx", &n) zwraca wszystkie pliki .dbx w katalogu. Każdy plik jest przetwarzany przez _undbx niezależnie. Folder wyjściowy dla każdego DBX to podfolder o nazwie równej nazwie pliku DBX bez rozszerzenia (np. Inbox.dbx → Inbox/).
DBXExtractor.exe "C:\OE\Inbox.dbx"
sys_glob nie zwróci wyników — num_files == 0. Funkcja wykrywa, że ścieżka wskazuje na istniejący plik przez sys_filesize_abs(*dir) != (ull)-1. Wyodrębniana jest część katalogowa (sys_dirname) jako nowy dbx_dir i bazowa nazwa pliku (sys_basename) jako jedyny element listy.
DBXExtractor.exe "C:\OE\*.bak"
strchr(*dir, '*') wykrywa wzorzec. sys_glob(dirpart, pattern, &n) zwraca wszystkie pasujące pliki. Następnie każdy plik jest weryfikowany przez _dbx_has_signature() — odczyt pierwszych 16 bajtów i sprawdzenie sygnatury. Pliki bez poprawnej sygnatury DBX są po cichu pomijane (free + usunięcie ze skompaktowanej tablicy).
int valid = 0;
for (int i = 0; i < *num_files; i++) {
if (_dbx_has_signature(dirpart, files[i]))
files[valid++] = files[i];
else
free(files[i]);
}
*num_files = valid;
Dzięki temu wildcard *.bak lub *.old działa transparentnie z archiwami DBX zmienionymi przez użytkownika.
Każdy dostęp do zmapowanego widoku przechodzi przez helpery z weryfikacją granic:
static inline int _mr_i32(const dbx_t *d, long off) {
int v = 0;
if (off >= 0 && (size_t)(off + 4) <= d->_sz)
memcpy(&v, d->_base + off, 4);
return v;
}
Odczyt spoza zakresu [0, _sz) zwraca 0 / zerowy bufor zamiast segfault. Pliki DBX z uszkodzonymi wskaźnikami nie powodują crashu — parser po prostu otrzymuje 0 i obsługuje go jako koniec łańcucha lub brak danych.
dbx_message() sprawdza bsz <= 0 || bsz > 0x200 dla każdego bloku. Przekroczenie limitu emituje DBX_STATUS_WARNING do logu i przerywa rekonstrukcję — plik EML może być niekompletny, ale parser nie zapętla się ani nie alokuje gigantycznych buforów.
Plik DBX o rozmiarze ≥ 2 GB (0x80000000 bajtów) jest traktowany jako uszkodzony — dbx_progress_message(DBX_STATUS_WARNING, "DBX file … is corrupted (> 2 GB)"). Ograniczenie wynika z faktu, że offsety wewnętrzne DBX są 32-bitowe (unsigned int), co przy zmapowaniu całego pliku i offsetach jako long / int32 daje maksymalny adresowalny offset ~2 GB.
Błąd _save_message (np. brak miejsca na dysku, brak uprawnień) jest raportowany przez DBX_STATUS_ERROR i inkrementuje licznik errors — nie przerywa przetwarzania pozostałych wiadomości. Końcowe podsumowanie dbx_progress_pop zawiera liczbę błędów: %d messages saved, %d skipped, %d errors.
#define WM_APPEND_LOG (WM_USER + 1) /* lParam = heap char* — przejęty przez UI, zwolniony */
#define WM_EXTRACTION_DONE (WM_USER + 2) /* wParam = 0:ok / 1:błędy */
#define WM_PROGRESS_UPDATE (WM_USER + 3) /* wParam = procent 0–100 */
Wątki robocze nigdy nie dotykają kontrolek GUI bezpośrednio — tylko PostMessage do g_hMainWnd. WM_APPEND_LOG niesie wskaźnik na heap char* — wątek UI odbiera go w WM_APPEND_LOG, wywołuje AppendLog(text) i free(text). Brak ryzyka race condition na stercie: alokacja w wątku roboczym, zwolnienie w wątku UI, jeden wyraźny moment przeniesienia własności.
g_extractingFlaga g_extracting = TRUE ustawiana jest przez StartExtraction() przed CreateThread. Resetowana jest przez WM_EXTRACTION_DONE w wątku UI. W tym czasie:
EnableWindow(g_hExtractBtn, FALSE)) i rysuje etykietę Working…VK_RETURN w WM_KEYDOWN jest ignorowane (!g_extracting)WM_CLOSE pyta o potwierdzenie ("Extraction is in progress. Exit anyway?")WM_EXTRACTION_DONE ustawia pasek na 100% i uruchamia SetTimer(hwnd, 2, 600, NULL). Po 600 ms WM_TIMER wywołuje ShowWindow(g_hProgress, SW_HIDE). Dzięki temu użytkownik widzi, że pasek dobił do 100%, zanim zniknie — feedback ukończenia bez natychmiastowego znikania.
| Moduł | Plik | Odpowiedzialność |
|---|---|---|
| Main | Main.cpp |
wWinMain: dispatch GUI / CLI na podstawie argc |
| CLI | CLI.cpp/.h |
SetupConsole, ParseArgs, PrintUsage, RunCLI |
| MainWindow | MainWindow.cpp/.h |
WindowProc (cały message loop GUI), RunGUI |
| Controls | Controls.cpp/.h |
BrowseFolder, BrowseFile, CreateControls, LayoutControls, AppendLog, StartExtraction |
| Extraction | Extraction.cpp/.h |
_undbx, _extract, _recover, _run_parallel_save, ExtractionThread, _get_files |
| Theme | Theme.cpp/.h |
InitDarkMode, DetectDarkMode, BuildPalette, ApplyMica; paleta Palette |
| Rendering | Rendering.cpp/.h |
PaintRoundButton, CreateUiFont, makro S(hwnd, dp) DPI-aware |
| HelpDialog | HelpDialog.cpp/.h |
ShowHelpDialog: pseudo-modalne okno z tekstem CLI help |
| AppGlobals | AppGlobals.cpp/.h |
globalne uchwyty kontrolek: g_hMainWnd, g_hProgress, g_hLogEdit, g_hExtractBtn, g_hStatusBar, g_isDark, g_extracting |
| AppCommon | AppCommon.h |
wspólne include Win32, stałe wersji, WM_USER messages |
| dbxread | dbxread.c/.h |
silnik parsowania DBX: dbx_open, dbx_close, dbx_message, dbx_recover_message; memory-mapped I/O; autodetekcja kodowania |
| dbxsys | dbxsys.c/.h |
PAL (Platform Abstraction Layer): sys_glob, sys_mkdir, sys_fopen, sys_set_filetime, utf8_to_wide |
| dbxprogress | dbxprogress.c/.h |
system raportowania postępu i logowania; callback dbx_output_fn_t dla GUI |
| emlread | emlread.c/.h |
eml_parse: ekstrakcja subject, from, to, timestamp z nagłówków RFC 2822 + dekodowanie RFC 2047 |
| undbx | undbx.c |
oryginalne CLI undbx (POSIX); w kompilacji Windows zastąpione przez Extraction.cpp |
DBXExtractor/
├── src/
│ ├── Main.cpp wWinMain → RunGUI / RunCLI dispatch
│ ├── AppCommon.h Win32 includes, wersja, WM_USER messages
│ ├── AppGlobals.cpp/.h globalne uchwyty kontrolek GUI
│ ├── resource.h IDC_ / IDR_ / IDI_ stałe zasobów
│ ├── DBXExtractor.rc zasoby: menu, ikona, version info
│ ├── cli/
│ │ ├── CLI.cpp SetupConsole, ParseArgs, RunCLI
│ │ └── CLI.h
│ ├── ui/
│ │ ├── MainWindow.cpp/.h WindowProc, RunGUI
│ │ ├── Controls.cpp/.h tworzenie / layout kontrolek, StartExtraction
│ │ ├── HelpDialog.cpp/.h pseudo-modalne okno pomocy CLI
│ │ ├── Rendering.cpp/.h PaintRoundButton, CreateUiFont, makro S()
│ │ └── Theme.cpp/.h dark mode, paleta, MICA
│ ├── core/
│ │ ├── Extraction.cpp/.h _undbx, _extract, _recover, wątek równoległy
│ │ ├── dbxread.c/.h parser DBX, mmap, autodetekcja kodowania
│ │ ├── dbxsys.c/.h PAL: glob, mkdir, fopen UTF-8, filetime
│ │ ├── dbxprogress.c/.h system logowania i postępu
│ │ └── emlread.c/.h parser RFC 2822 / RFC 2047
│ └── icon/
│ └── Icon.ico
├── DBXExtractor.vcxproj projekt MSVC v145, C++23, x64/x86
├── DBXExtractor.vcxproj.filters filtry Solution Explorer
├── DBXExtractor.sln plik solution
└── build.ps1 skrypt MSBuild PowerShell
.\build.ps1
Lub bezpośrednio przez MSBuild:
msbuild DBXExtractor.vcxproj /p:Configuration=Release /p:Platform=x64
| Konfiguracja | Optymalizacja | CRT |
|---|---|---|
| Debug | x64 | wyłączona | dynamiczny /MDd |
| Release | x64 | MaxSpeed /O2 |
statyczny /MT |
| Debug | Win32 | wyłączona | dynamiczny /MDd |
| Release | Win32 | MaxSpeed /O2 |
statyczny /MT |
Flagi kompilatora stosowane we wszystkich konfiguracjach:
/utf-8 kodowanie źródła i wykonania = UTF-8
/W4 wysoki poziom ostrzeżeń
/permissive- ścisła zgodność ISO C++
/sdl dodatkowe kontrole bezpieczeństwa
/std:c++latest C++23
/Zc:__cplusplus __cplusplus odzwierciedla faktyczny standard
/Zc:preprocessor nowy preprocesor zgodny z normą
Release /MT zapewnia brak zależności od redystrybuowalnych pakietów VC++. Wynikowy DBXExtractor.exe działa na każdym Windows 10+ bez instalatorów.
| Składnik | Wymaganie |
|---|---|
| System operacyjny | Windows 10 lub nowszy (MICA wymaga Win 11 build 22000+) |
| Architektura | x64 (projekt domyślny) lub Win32 |
| Zależności zewnętrzne | brak — wyłącznie WinAPI (comctl32, dwmapi, uxtheme, shell32, comdlg32) |
| Pliki wejściowe | pliki .dbx z Outlook Express 4, 5 lub 6 |
| Pliki wyjściowe | pliki .eml (RFC 2822) otwieralne przez Microsoft Outlook |
| Uprawnienia | tylko do odczytu dla DBX; zapis do folderu docelowego |
| Poziom | Stała | Opis |
|---|---|---|
| 0 | DBX_VERBOSITY_QUIET |
brak wyjścia |
| 1 | DBX_VERBOSITY_ERROR |
tylko błędy krytyczne |
| 2 | DBX_VERBOSITY_WARNING |
błędy + ostrzeżenia |
| 3 | DBX_VERBOSITY_INFO |
(domyślny) — informacje o ekstrakcji |
| 4 | DBX_VERBOSITY_VERBOSE |
szczegóły każdej wiadomości |
| 5 | DBX_VERBOSITY_DEBUG |
pełne debugowanie parsera |
W GUI poziom wybierany jest comboboksem. W CLI flagą -v N / --verbosity N. Flaga -d / --debug jest skrótem dla poziomu 5 niezależnie od -v.
dbxprogress.c definiuje pięć statusów raportowanych przez dbx_progress_update():
| Status | Stała | Znaczenie |
|---|---|---|
| ✓ | DBX_STATUS_OK |
wiadomość zapisana pomyślnie |
| ✗ | DBX_STATUS_ERROR |
błąd zapisu pliku EML |
| ⚠ | DBX_STATUS_WARNING |
ostrzeżenie (plik pominięty, uszkodzony wpis) |
| → | DBX_STATUS_MOVED |
plik EML przeniesiony do deleted/ |
| 🗑 | DBX_STATUS_DELETED |
plik EML usunięty (--delete) |
W GUI statusy trafiają do logu przez callback GuiProgressCallback → PostMessageA(WM_APPEND_LOG) → AppendLog() → SetWindowTextW na ES_MULTILINE. Linie zaczynające się od \nN% są parsowane jako aktualizacja paska; czyste pingowanie postępu (nic po %) nie trafia do logu — tylko aktualizuje pasek.
DBXExtractor jest przepisanym i rozszerzonym potomkiem dwóch projektów open source:
| Projekt | Autor | Rola w DBXExtractor |
|---|---|---|
| UnDBX | Avi Rozen | oryginalne CLI do ekstrakcji DBX, algorytmy _extract, _recover, sys_* PAL |
| DbxConv (DbxRead) | Ulrich Krebs | parser formatu binarnego DBX: B-drzewo indeksu, dekodowanie pól dbx_info_t, rekonstrukcja łańcuchów bloków |
| GNU Mailutils (podzbiór) | Free Software Foundation | parser RFC 2822 / RFC 2047 w emlread.c: decoded-word, QP, Base64 |
Kluczowe zmiany wprowadzone w DBXExtractor względem oryginałów:
fseek/fread przez MapViewOfFile + inline helpery z bounds checking. Efekt: brak fseek w kodzie parsera, dostęp przez wskaźniki, demand paging przez OS.CreateThread × N CPU), InterlockedIncrement, absolutne ścieżki eliminujące sys_chdir z wątków roboczych.user → detected → CP_ACP.wWinMain dispatch na argc), identyczny silnik ekstrakcji.DBXExtractor v1.0.0.1 — WinAPI, C/C++23, zero zewnętrznych zależności.
kvc.pl | marek@kvc.pl