Menulis Ulang z-push ke Rust, Khusus untuk Carbonio CE

Repo: github.com/afatyoo/carbonio-ce-zpush (MIT license)

Minggu ini saya rilis v1.0.0 dari sebuah proyek yang saya kerjakan beberapa waktu terakhir: konektor ActiveSync untuk Zextras Carbonio CE, ditulis ulang dari nol pakai Rust. Tulisan ini ceritanya, dari kenapa saya mulai, apa yang saya temui pas development, sampai bug bug yang baru ketahuan setelah dites ke HP asli.

Masalahnya apa

Kalau kalian pernah pakai Zimbra atau Carbonio dan mau sinkron email lewat Exchange ActiveSync ke HP (bukan IMAP), kemungkinan besar kalian pakai z-push. Itu konektor PHP yang sudah ada dari lama, dan untuk backend Zimbra, ada fork khusus yang saya pakai sebagai referensi awal.

Masalahnya, z-push itu besar. Sekitar 100 ribu baris PHP, dan sebagian besar dari itu untuk backend backend yang saya tidak pernah pakai: Kopano, LDAP, CalDAV, CardDAV, maildir. Semua itu ikut terinstall meski yang saya butuh cuma satu jalur, dari ActiveSync ke Carbonio. Supaya HP bisa sinkron, server harus pasang PHP runtime penuh, web server, dan seluruh dependency tree itu, cuma untuk satu protokol.

Saya pikir, kalau cuma satu jalur yang dipakai, kenapa tidak ditulis ulang khusus untuk itu saja. Jadi saya mulai nulis ulang bagian ActiveSync-nya saja, khusus untuk Carbonio CE, pakai Rust.

Kenapa Rust

Ada beberapa alasan konkret, bukan sekadar ikut tren.

Tidak ada PHP di server. cargo build menghasilkan satu binary statis. Tinggal copy ke server, jalankan, selesai. Tidak perlu php-fpm, tidak perlu Composer, tidak perlu konfigurasi Apache atau nginx khusus buat z-push.

WBXML itu protokol biner, dan parsernya baca input yang tidak bisa dipercaya. Setiap body command ActiveSync itu byte mentah dari HP atau Outlook, dan parsernya harus jalan token demi token, termasuk pergantian codepage dan string berawalan panjang. Di PHP, satu byte yang salah bentuk bisa merusak state secara diam diam atau bikin worker crash. Di Rust, setiap langkah decode di wbxml.rs mengembalikan Result, jadi kalau streamnya rusak atau aneh, itu jadi error bertipe yang harus ditangani pemanggilnya, bukan tebak tebakan.

Compiler yang nangkep kesalahan protokol sebelum device yang nangkep. Di codepages.rs, tiap tag WBXML didefinisikan sekali, dan ada test yang mengecek semua tabel encode dan decode sepakat soal token yang sama untuk nama tag yang sama. Kalau ada kode tag yang salah, test-nya gagal pas cargo test, bukan tiga minggu kemudian pas ada iPhone tertentu yang tiba tiba tidak bisa buka Inbox.

Satu proses bisa pegang banyak device tanpa biaya seperti PHP-FPM. Tiap HP yang konek itu nahan satu sesi sync tetap terbuka. Model PHP-FPM itu satu proses atau thread per request. Tokio jalanin semua sesi itu sebagai async task dalam satu proses, jadi jumlah device yang bisa ditangani tidak ikut naikin jumlah proses worker.

Fase localhost itu menipu

Begitu command set utama (FolderSync, Sync, SendMail, Ping, dan lain lain) selesai dan semua test WBXML buatan sendiri lolos, rasanya sudah selesai. Tapi itu justru fase paling menipu dari seluruh proyek ini.

Saya nulis test WBXML sendiri berdasarkan apa yang saya kira device akan kirim, bukan apa yang device sungguhan kirim. Jadi semua test itu konsisten sama asumsi saya sendiri, bukan sama kenyataan. Baru ketahuan bedanya begitu saya setup infrastruktur buat tes ke HP Android asli lewat internet (WireGuard tunnel ke VPS publik, nginx reverse proxy dengan sertifikat Let’s Encrypt asli, baru dari situ HP bisa konek).

Dari situ, satu per satu bug baru muncul yang tidak pernah kesentuh sama test saya sendiri. Catatan jujur: dari titik ini, debugging-nya saya kerjakan bareng Claude (AI assistant dari Anthropic), bukan murni sendirian. Capture byte, baca log server secara live, dan decode WBXML byte demi byte itu saya lakukan bareng Claude, bukan cuma saya sendiri di depan hex editor.

Device tidak pernah nyampe ke server

Yang pertama bahkan tidak sempat masuk log server. Device ActiveSync asli selalu kirim request OPTIONS dulu sebelum coba POST pertama, buat ngecek command apa saja yang server-nya dukung. Router saya cuma punya route buat POST. Jadi layar setup di HP langsung gagal, dan server saya sama sekali tidak kebagian log apa apa, karena request-nya memang tidak pernah sampai sejauh itu. Script test saya sendiri selalu langsung POST, jadi gap ini tidak pernah kelihatan sampai HP asli yang coba.

Loop resync yang tidak pernah berhenti

Ini yang paling bikin pusing waktu debug. Saat Sync ngecek apakah sebuah pesan punya lampiran, saya panggil GetMsgRequest ke Carbonio. Ternyata, secara default, call itu diam diam menandai pesan sebagai sudah dibaca, walau saya tidak pernah minta itu. Penandaan baca itu bikin counter perubahan folder di Carbonio ikut naik, dan logic FolderSync saya membaca itu sebagai “hierarki folder berubah”, yang bikin SyncKey tiap collection di-reset ke 0, di setiap poll. Hasilnya, HP saya kedap kedip, full resync berulang tiap beberapa detik, tidak pernah settle ke status tenang. Ketahuan bukan dari membaca kode, tapi dari nonton log server secara live sambil HP-nya sinkron, dan bandingin counter per folder sebelum dan sesudah tiap request.

Semua folder ngaku jadi Inbox

Tipe folder ActiveSync yang saya kirim ke device cuma lihat atribut view dari Carbonio. Masalahnya, Inbox, Sent, Drafts, dan Trash semuanya punya view yang sama, jadi semuanya dikirim ke device dengan tipe yang sama persis, “Inbox utama”. Device tidak terima kalau ada lebih dari satu folder yang ngaku sebagai Inbox utama. Jadi seluruh daftar foldernya kelihatan kontradiktif dari sisi HP, dan device itu juga tidak pernah bisa settle. Perbaikannya pakai id folder sistem bawaan Carbonio yang memang tetap di tiap akun, bukan cuma view.

Isi email yang dikirim hilang

Awalnya saya asumsikan body SendMail itu teks RFC 822 mentah, persis seperti yang saya kirim di test saya sendiri. Ternyata Gmail app di Android membungkus seluruh pesan itu sebagai data binary opaque di dalam amplop WBXML, bukan teks biasa. Saya cuma ketemu ini dengan capture byte mentah dari request kirim asli, lalu decode bareng Claude (AI assistant yang saya pakai sepanjang proyek ini), byte demi byte, sampai ketemu polanya.

Body email yang kadang ada kadang tidak

Ini yang paling lama saya debug, karena polanya tidak langsung kelihatan. Sebagian email yang disinkronkan ke HP punya isi, sebagian kosong, tanpa pola yang jelas di awal. Setelah digali, ternyata Carbonio menandai MIME part mana pun yang muncul duluan sebagai “body”, meski isinya kosong, sementara konten asli ada tepat di sebelahnya di bagian HTML. Dan terpisah dari itu, Carbonio memang tidak akan kirim isi HTML itu sama sekali kecuali diminta eksplisit lewat parameter html=1 di request-nya. Tanpa parameter itu, field-nya bukan cuma kosong, tapi memang tidak ada sama sekali di response.

Event kalender yang hilang tanpa jejak

Yang ini paling aneh didebug, karena tidak ada error di mana pun. Bikin event kalender di HP tidak pernah nyampe ke Carbonio. Ternyata server saya diam diam membuang permintaan “Add” dari device untuk item baru, tapi tetap menjawab status sukses. Jadi device percaya event-nya sudah dibuat, dan tidak pernah coba kirim ulang. Setelah diperbaiki supaya permintaan Add benar benar diproses, baru ketahuan juga Calendar codepage yang saya pakai cuma mendefinisikan field yang saya butuh untuk arah baca, device asli ternyata kirim field lain juga (timezone, status organizer, dan beberapa lagi) yang kalau tidak dikenali, bikin seluruh pesannya gagal didecode.

Pelajarannya

Semua bug di atas ketemu dengan cara yang sama persis: capture byte mentah dari request device asli, decode bareng Claude, bandingkan sama apa yang saya kira sebelumnya. Test saya sendiri cuma bisa membuktikan kode saya konsisten sama tebakan saya sendiri soal apa yang device kirim. Butuh device asli yang sungguhan bicara, dan AI yang bantu ngubek byte demi byte bareng saya, buat tahu tebakan mana yang salah.

Kalau kalian lagi bangun sesuatu yang harus bicara dengan sistem orang lain lewat protokol biner, baik itu ActiveSync, atau protokol lain yang spesifikasinya setebal buku, test yang kalian tulis sendiri cuma seberapa baik kalian sejalan dengan asumsi sendiri. Baru pas device atau client asli yang coba, ketahuan mana yang beneran bener.

Status sekarang

v1.0.0 sudah feature complete untuk command set MS-ASCMD yang jadi target proyek ini (FolderSync, Sync untuk email/kontak/kalender dengan delta sync asli, SendMail, Ping, ItemOperations, MoveItems, ResolveRecipients, Settings, Search), dan semuanya sudah diverifikasi langsung ke Carbonio CE yang hidup, bukan cuma WBXML buatan sendiri di localhost. Detail lengkap tiap fix ada di CHANGELOG repo-nya.

Repo-nya open source, lisensi MIT: github.com/afatyoo/carbonio-ce-zpush

Leave a Reply

Your email address will not be published. Required fields are marked *