true-imap-tunnel/android-plugin
2026-06-01 17:34:15 +02:00
..
app Release v0.4.0 2026-06-01 17:34:15 +02:00
build.gradle.kts Release v0.1.1 2026-05-24 23:30:48 +02:00
ci-debug.keystore Release v0.4.0 2026-06-01 17:34:15 +02:00
gradle.properties Release v0.1.1 2026-05-24 23:30:48 +02:00
README.md Release v0.4.0 2026-06-01 17:34:15 +02:00
settings.gradle.kts Release v0.1.1 2026-05-24 23:30:48 +02:00

True IMAP Tunnel Shadowsocks Android plugin

This directory contains the Android wrapper that exposes the true-imap-tunnel Go binary as a SIP003 Shadowsocks native plugin.

The plugin id is:

true-imap-tunnel

The plugin reads normal SIP003 environment variables, especially SS_LOCAL_HOST, SS_LOCAL_PORT, SS_REMOTE_HOST, SS_REMOTE_PORT, and SS_PLUGIN_OPTIONS.

Android client hints

In Shadowsocks Android, it is strongly recommended to enable "Apps VPN Mode" / per-app VPN and route only the apps that really need this tunnel. IMAP is not a good bulk transport, so do not invite every background service on the phone to the party.

Also enable "Metered Hint" in the profile settings. It helps Android and apps treat the path as expensive and avoid unnecessary background traffic.

Set profile "Remote DNS" to an IP literal such as 1.1.1.1 or 8.8.8.8, not dns.google. Using the IP avoids one DNS lookup before DNS-over-proxy starts.

If plugin DNS itself fails, set dns_server=1.1.1.1 in plugin options or put dns_servers: ["1.1.1.1"] in YAML. If no DNS is configured and the local resolver fails at startup, the plugin falls back to 1.1.1.1:53.

In Shadowsocks Android VPN mode, the app injects the SIP003 option __android_vpn. The plugin detects it and protects outbound IMAP/DNS sockets through Shadowsocks' protect_path socket so its own transport traffic does not loop back into the VPN.

The APK also has a tiny launcher app. Open it while the Shadowsocks profile is running to see current tunnel/account status and recent logs. The plugin exposes this only on the device loopback address http://127.123.45.67:17680; set status_addr=off in plugin options if you need to disable it.

Plugin config formats

There are two supported SS_PLUGIN_OPTIONS styles.

Base64 YAML

config=<base64url-yaml>

This embeds the normal YAML config. When generated by the CLI, comments are stripped first so the URL stays smaller. This is the best format for full configs and multipath.

The decoder also accepts:

config_b64=<base64url-yaml>
yaml_b64=<base64url-yaml>
tits://<base64url-yaml>

Query/inline options

imap_host=imap.example.com:993;
imap_username=user@example.com;
imap_password=app-password;
folder_send=TunnelC2S;
folder_recv=TunnelS2C

Useful optional keys:

mode=client
imap_tls=implicit
zero_rtt_open=false
async_data_send=true
batch_delay_ms=2
dns_server=1.1.1.1
message_format=attachment
multipath_mode=stream_affinity
client_version=custom-build-label

Multiple accounts use suffixes: imap_host_2, imap_username_2, folder_send_2, and so on. The first account may use either unsuffixed keys or _1.

client_id may be supplied explicitly. On clients such as Shadowsocks Android, SS_REMOTE_PORT is also exposed to the plugin; we map that port to a client id byte with port % 256, with 0 mapped to 255. This lets multiple users share the same YAML profile while still stamping distinct stream IDs.

zero_rtt_open defaults to off for SIP003 profiles. You can turn it on, but test the profile with your actual app traffic first.

If you are tunneling Shadowsocks, you can usually leave encryption_passphrase empty. Shadowsocks traffic is already encrypted, and the extra tunnel encryption layer only adds CPU work and a few bytes of overhead per frame.

Generate Shadowsocks URLs

Generate an importable ss:// URL from a YAML client config:

.\bin\true-imap-tunnel.exe -config .\client.yaml -show-ss-url `
  -ss-method aes-128-gcm `
  -ss-password "shadowsocks-profile-password" `
  -ss-url-format base64 `
  -ss-url-skip-defaults

Generate query/inline options instead:

.\bin\true-imap-tunnel.exe -config .\client.yaml -show-ss-url `
  -ss-method aes-128-gcm `
  -ss-password "shadowsocks-profile-password" `
  -ss-url-format query `
  -ss-url-skip-defaults

The Shadowsocks profile host is a placeholder for this plugin and defaults to tits.local. The profile port defaults to 443; on Shadowsocks Android it also becomes the client_id override described above.

Use a method accepted by the Android Shadowsocks importer, such as aes-128-gcm, aes-256-gcm, or chacha20-ietf-poly1305.

Getting logs

If the Android plugin profile fails to connect, first add this to plugin options or YAML for one short test run:

log_level=debug

Use log_level=trace only when you need very noisy protocol details.

The Go plugin writes logs to stdout/stderr, and Shadowsocks Android usually captures those lines in logcat together with its own service logs. Clear logcat, start the profile, reproduce the problem, then dump the interesting lines:

$adb = "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe"
& $adb logcat -c

# Start the Shadowsocks profile now, reproduce the issue, then:
& $adb logcat -d -v time |
  Select-String -Pattern "true-imap-tunnel|trueimaptunnel|libtrueimaptunnel|tits|Shadowsocks|sslocal|plugin"

For a live view:

& $adb logcat -v time |
  Select-String -Pattern "true-imap-tunnel|trueimaptunnel|libtrueimaptunnel|tits|Shadowsocks|sslocal|plugin"

On Linux/macOS:

adb logcat -c
adb logcat -v time | grep -Ei 'true-imap-tunnel|trueimaptunnel|libtrueimaptunnel|tits|shadowsocks|sslocal|plugin'

To focus on the Shadowsocks Android app process, find its process id:

& $adb shell pidof com.github.shadowsocks

Then use that PID with logcat if your Android version supports it:

& $adb logcat -v time --pid <pid>

Also check the Shadowsocks Android UI log screen if your version has one. When sharing logs, remove IMAP usernames, tokens, passwords, and generated ss:// URLs.

Build

Requirements:

  • Android SDK installed and ANDROID_HOME or ANDROID_SDK_ROOT pointing to it.
  • Gradle 8.x available on PATH, or an installed Gradle distribution.
  • Go installed.

Build debug APK:

$env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk"
gradle -p android-plugin assembleDebug --no-daemon --console=plain

Build release APK:

$env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk"
gradle -p android-plugin assembleRelease --no-daemon --console=plain

Release APKs are unsigned unless you configure Android signing. CI publishes the signed debug APK (TITS-shadowsocks-plugin-android-debug.apk) so downloaded artifacts can be sideloaded directly. Debug builds use the checked-in public ci-debug.keystore, which keeps CI artifact signatures stable across builds. If you previously installed a locally signed or older CI-signed plugin, fully uninstall com.trueimaptunnel.plugin from every Android user/profile before installing the new artifact.

To clear all profiles with ADB, list users and uninstall the package for each listed ID:

$adb = "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe"
& $adb shell pm list users
& $adb shell pm uninstall --user 0 com.trueimaptunnel.plugin
& $adb shell pm uninstall --user 10 com.trueimaptunnel.plugin

Failure [not installed for N] is fine for profiles where the plugin is already absent. After the per-profile uninstall, this check should print no matching package:

& $adb shell pm list packages -u | Select-String 'trueimap|true-imap|tits'

The Gradle build runs scripts/build-android-plugin-binary.ps1 on Windows or scripts/build-android-plugin-binary.sh elsewhere to compile the Android arm64 Go binary and stage it as:

android-plugin/app/src/main/jniLibs/arm64-v8a/libtrueimaptunnel.so

Debug APK output:

android-plugin/app/build/outputs/apk/debug/app-debug.apk

Install to a connected device:

& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" install -r `
  ".\android-plugin\app\build\outputs\apk\debug\app-debug.apk"

The Android APK build stamps its versionName, git hash, and build date into the Go binary. CI passes versionName from the release tag, stripping a leading v, or from the workflow-dispatch build version override. For local builds that should report a specific version, pass -PbuildVersion=0.4.0 to Gradle. Client Ping frames include that build string after the timestamp, and servers log it as client_version=.... Set client_version manually only for custom wrappers or manual builds that need a different label.