├── .github
└── workflows
│ └── checks.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── fastlane
└── metadata
│ └── android
│ └── en-US
│ ├── changelogs
│ ├── 14.txt
│ ├── 15.txt
│ ├── 18.txt
│ ├── 20.txt
│ ├── 21.txt
│ ├── 22.txt
│ ├── 23.txt
│ ├── 24.txt
│ ├── 25.txt
│ ├── 26.txt
│ ├── 27.txt
│ ├── 28.txt
│ ├── 29.txt
│ ├── 30.txt
│ ├── 31.txt
│ ├── 32.txt
│ ├── 33.txt
│ ├── 34.txt
│ ├── 35.txt
│ ├── 36.txt
│ ├── 37.txt
│ ├── 38.txt
│ ├── 39.txt
│ ├── 40.txt
│ ├── 41.txt
│ └── 42.txt
│ ├── full_description.txt
│ ├── images
│ ├── icon.png
│ └── phoneScreenshots
│ │ ├── screenshot-01.png
│ │ ├── screenshot-02.png
│ │ ├── screenshot-03.png
│ │ ├── screenshot-04.png
│ │ └── screenshot-05.png
│ └── short_description.txt
├── scripts
├── build-podman-exec.sh
├── build-podman.sh
└── podman-do.sh
├── you-have-mail-android
├── .gitignore
├── README.md
├── app
│ ├── .gitignore
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src
│ │ ├── androidTest
│ │ └── java
│ │ │ └── dev
│ │ │ └── lbeernaert
│ │ │ └── youhavemail
│ │ │ └── ExampleInstrumentedTest.kt
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── ic_launcher-playstore.png
│ │ ├── java
│ │ │ └── dev
│ │ │ │ └── lbeernaert
│ │ │ │ └── youhavemail
│ │ │ │ ├── .gitignore
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── OpenAppActivity.kt
│ │ │ │ ├── app
│ │ │ │ ├── ActionReceivers.kt
│ │ │ │ ├── ActionWorker.kt
│ │ │ │ ├── Backends.kt
│ │ │ │ ├── LoginSequence.kt
│ │ │ │ ├── Logs.kt
│ │ │ │ ├── Notifications.kt
│ │ │ │ ├── PollWorker.kt
│ │ │ │ ├── StartReceiver.kt
│ │ │ │ ├── State.kt
│ │ │ │ └── YhmInstance.kt
│ │ │ │ ├── components
│ │ │ │ ├── ActionButton.kt
│ │ │ │ ├── AsyncScreen.kt
│ │ │ │ ├── BackgroundTask.kt
│ │ │ │ └── PasswordField.kt
│ │ │ │ ├── screens
│ │ │ │ ├── AccountInfo.kt
│ │ │ │ ├── BackendSelection.kt
│ │ │ │ ├── Login.kt
│ │ │ │ ├── Main.kt
│ │ │ │ ├── Nav.kt
│ │ │ │ ├── ProtonCaptcha.kt
│ │ │ │ ├── Proxy.kt
│ │ │ │ ├── Routes.kt
│ │ │ │ ├── Settings.kt
│ │ │ │ └── Totp.kt
│ │ │ │ └── ui
│ │ │ │ ├── Autofill.kt
│ │ │ │ └── theme
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Shape.kt
│ │ │ │ ├── Theme.kt
│ │ │ │ └── Type.kt
│ │ └── res
│ │ │ ├── drawable-hdpi
│ │ │ ├── ic_stat_alert.png
│ │ │ ├── ic_stat_err.png
│ │ │ └── ic_stat_sync.png
│ │ │ ├── drawable-mdpi
│ │ │ ├── ic_stat_alert.png
│ │ │ ├── ic_stat_err.png
│ │ │ └── ic_stat_sync.png
│ │ │ ├── drawable-xhdpi
│ │ │ ├── ic_stat_alert.png
│ │ │ ├── ic_stat_err.png
│ │ │ └── ic_stat_sync.png
│ │ │ ├── drawable-xxhdpi
│ │ │ ├── ic_stat_alert.png
│ │ │ ├── ic_stat_err.png
│ │ │ └── ic_stat_sync.png
│ │ │ ├── drawable-xxxhdpi
│ │ │ ├── ic_stat_alert.png
│ │ │ ├── ic_stat_err.png
│ │ │ └── ic_stat_sync.png
│ │ │ ├── drawable
│ │ │ └── ic_base.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_background.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_background.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_background.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_background.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_background.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── values
│ │ │ ├── colors.xml
│ │ │ ├── ic_launcher_background.xml
│ │ │ ├── strings.xml
│ │ │ └── themes.xml
│ │ │ └── xml
│ │ │ ├── backup_rules.xml
│ │ │ └── data_extraction_rules.xml
│ │ └── test
│ │ └── java
│ │ └── dev
│ │ └── lbeernaert
│ │ └── youhavemail
│ │ └── ExampleUnitTest.kt
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── icon_inkscape.svg
└── settings.gradle
└── you-have-mail-mobile
├── Cargo.lock
├── Cargo.toml
├── rust-toolchain.toml
├── src
├── account.rs
├── android.rs
├── backend
│ ├── mod.rs
│ └── proton.rs
├── events.rs
├── lib.rs
├── logging.rs
├── proxy.rs
├── watcher.rs
└── yhm.rs
├── uniffi-bindgen
└── uniffi-bindgen.rs
└── uniffi.toml
/.github/workflows/checks.yml:
--------------------------------------------------------------------------------
1 | name: Code Checks
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | types: [ opened, synchronize, reopened ]
9 | branches:
10 | - main
11 |
12 | env:
13 | CARGO_TERM_COLOR: always
14 |
15 | jobs:
16 | test:
17 | name: Build
18 | runs-on: ubuntu-latest
19 | steps:
20 | - name : Get sources
21 | uses: actions/checkout@v3
22 |
23 | - name: Setup Go 1.19
24 | uses: actions/setup-go@v3
25 | with:
26 | go-version: '1.20'
27 |
28 | - name: Setup Rust
29 | uses: dtolnay/rust-toolchain@1.83.0
30 |
31 | - name: Run Tests
32 | working-directory: you-have-mail-mobile
33 | run: cargo build
34 |
35 |
36 | fmt:
37 | name: Rustmft
38 | runs-on: ubuntu-latest
39 | steps:
40 | - name : Get sources
41 | uses: actions/checkout@v3
42 |
43 | - name: Setup Rust
44 | uses: dtolnay/rust-toolchain@stable
45 |
46 | - name: Check formatting
47 | working-directory: you-have-mail-mobile
48 | run: cargo fmt --check
49 |
50 | clippy:
51 | name: Clippy
52 | runs-on: ubuntu-latest
53 | steps:
54 | - name : Get sources
55 | uses: actions/checkout@v3
56 |
57 | - name: Setup Rust
58 | uses: dtolnay/rust-toolchain@stable
59 |
60 | - name: Check formatting
61 | working-directory: you-have-mail-mobile
62 | run: cargo clippy -- -D warnings
63 |
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | .idea
3 | /artifacts
4 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM debian:bookworm
2 |
3 | ENV ANDROID_BUILD_TOOLS_VERSION 33.0.1
4 | ENV ANDROID_HOME /opt/android/sdk
5 | ENV ANDROID_NDK_DIR /opt/android/ndk
6 | ENV ANDROID_NDK_HOME $ANDROID_HOME/ndk
7 | ENV ANDROID_VERSION 34
8 | ENV PATH $PATH:$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/bin
9 | ENV ANDROID_NDK_VERSION "25.2.9519653"
10 | ENV RUST_VERSION="1.85.0"
11 |
12 |
13 | RUN apt-get update && \
14 | apt-get install -y --no-install-recommends unzip curl sdkmanager && \
15 | mkdir -p $ANDROID_HOME $ANDROID_NDK
16 |
17 | # Install build deps
18 | RUN apt-get install -y automake clang gcc-multilib libclang-dev \
19 | libtool make pkg-config python-is-python3 \
20 | openjdk-17-jdk-headless
21 |
22 | # Install SDK
23 | RUN yes | sdkmanager --licenses --sdk_root=$ANDROID_HOME && \
24 | sdkmanager --sdk_root=$ANDROID_HOME \
25 | "build-tools;${ANDROID_BUILD_TOOLS_VERSION}" \
26 | "platforms;android-${ANDROID_VERSION}" \
27 | "platform-tools" \
28 | "ndk;$ANDROID_NDK_VERSION"
29 |
30 | # Install rust
31 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain "$RUST_VERSION"
32 |
33 | # Install git
34 | RUN apt-get install -y git
35 |
36 | # Cleanup
37 | RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
38 | apt-get autoremove -y && \
39 | apt-get clean
40 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # You Have Mail
2 |
3 | Small application to notify you when you receive an email in your email account. This may be useful for cases where
4 | you only wish to be notified when your email account has a new message and the default notification mechanism
5 | does not work (e.g: Android without Google Play Services) or do not wish to have the web interface/email client open at
6 | all times.
7 |
8 | If you want these features in CLI package, please see [You Have Mail CLI](https://github.com/LeanderBB/you-have-mail-cli).
9 |
10 | ## Supported Backends
11 |
12 | The application structure has been made backend agnostics, so it should be possible to add different providers in the
13 | future. Currently, the following email providers are supported:
14 |
15 | * [Proton Mail](https://mail.proton.me)
16 |
17 | ## Structure
18 |
19 | This repository is split into the following projects:
20 |
21 | * [you-have-mail-mobile](you-have-mail-mobile): Shared code for mobile bindings
22 | * [you-have-mail-android](you-have-mail-android): Android Application
23 |
24 | ## Download
25 |
26 | Please only download the latest stable releases from:
27 |
28 | * Github: [Releases](https://github.com/LeanderBB/you-have-mail/releases)
29 | * F-Droid: [Link](https://f-droid.org/packages/dev.lbeernaert.youhavemail/)
30 |
31 | [
](https://f-droid.org/packages/dev.lbeernaert.youhavemail/)
32 |
33 |
34 | ## Donations
35 |
36 | If you wish to donate to this project, consider donating to the
37 | [GrapheneOS](https://grapheneos.org/donate) project instead :).
38 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/14.txt:
--------------------------------------------------------------------------------
1 | * Only use Inbox Style notification when more than 1 message is available.
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/15.txt:
--------------------------------------------------------------------------------
1 | * Add fastlane metadata for FDroid inclusion
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/18.txt:
--------------------------------------------------------------------------------
1 | * Feat: Only send notifications for messages that have not
2 | been seen on other clients.
3 | * Feat: Remove Rust async runtime in favor of blocking code.
4 | Might have some benefits for battery life.
5 | * Fix: When no network is available release wake lock to allow
6 | the CPU to go into idle. Helps with battery life.
7 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/20.txt:
--------------------------------------------------------------------------------
1 | * Changes to ensure reproducible builds between fdroid and github
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/21.txt:
--------------------------------------------------------------------------------
1 | * Auto start foreground service when Phone reboots
2 | * Clarify that Proxy settings are Optional
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/22.txt:
--------------------------------------------------------------------------------
1 | * Feature: Handle Proton Captcha verification
2 | * Fix: Do not report Logout notification when user is logged out due to user action
3 | * Fix: Distinguish request parser error from actual network error
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/23.txt:
--------------------------------------------------------------------------------
1 | * Fix deadlock on proton account auto session refresh
2 | * Remove Proton Other Backend, no longer required with the introduction of Captcha support
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/24.txt:
--------------------------------------------------------------------------------
1 | * Fix login for Proton accounts which have both TOTP and FIDO2 as 2FA
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/25.txt:
--------------------------------------------------------------------------------
1 | # Features
2 | * Log files: Application can now record and share logs for debugging
3 | * Password field visibility toggle
4 | * Monochrome Icon (Android 13+)
5 |
6 | # Fixes
7 | * Improve app state management
8 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/26.txt:
--------------------------------------------------------------------------------
1 | # Fixes
2 | * Fix account ordering after adding new account
3 |
4 | # Misc
5 | * Tweak log output
6 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/27.txt:
--------------------------------------------------------------------------------
1 | # Features
2 |
3 | * Improve battery life by using Android Work Manager to drive the polling of accounts.
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/28.txt:
--------------------------------------------------------------------------------
1 | # Features
2 |
3 | * Add support for new mail notification in custom folders which have notifications enabled for
4 | Proton Mail accounts.
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/29.txt:
--------------------------------------------------------------------------------
1 | # Features
2 |
3 | * Add support for 32Bit Arm CPUs
4 | * Add Refresh button to poll accounts now rather than wait for the next scheduled poll.
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/30.txt:
--------------------------------------------------------------------------------
1 | # Fixes
2 |
3 | * Improve handling of session refreshes when there is no network.
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/31.txt:
--------------------------------------------------------------------------------
1 | # Features
2 | * Auto fill support (Thanks @thgoebel)
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/32.txt:
--------------------------------------------------------------------------------
1 | # Notice
2 |
3 | * All accounts will be in logged out state after migration
4 | to new storage format.
5 |
6 | # Changed
7 |
8 | * Removed background service and notification
9 | * Polling is now preformed by WorkManager
10 | * Account state is persisted to disk between OS reboots
11 | * Update dependencies and deprecated APIs
12 |
13 | # Fixes
14 |
15 | * Startup crash when starting service from a worker
16 | * Fix proxy screen not remembering values
17 | * Https is not a valid proxy protocol
18 |
19 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/33.txt:
--------------------------------------------------------------------------------
1 | # Notice
2 |
3 | * All accounts will be in logged out state after migration
4 | to new storage format.
5 |
6 | # Changed
7 |
8 | * Removed background service and notification
9 | * Polling is now preformed by WorkManager
10 | * Account state is persisted to disk between OS reboots
11 | * Update dependencies and deprecated APIs
12 |
13 | # Fixes
14 |
15 | * Startup crash when starting service from a worker
16 | * Fix proxy screen not remembering values
17 | * Https is not a valid proxy protocol
18 |
19 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/34.txt:
--------------------------------------------------------------------------------
1 | # Fixed
2 |
3 | * Register workers when boot signal is received
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/35.txt:
--------------------------------------------------------------------------------
1 | # Fixed
2 |
3 | * Some notifications not being delivered
4 | * Do not cancel work on startup if no change has been made to poll interval
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/36.txt:
--------------------------------------------------------------------------------
1 | # Fixed
2 |
3 | - Fixed poll not triggering for intervals less than 15 min.
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/37.txt:
--------------------------------------------------------------------------------
1 | # Changed
2 |
3 | * Dependency updates
4 | * Improve account list update
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/38.txt:
--------------------------------------------------------------------------------
1 | # Changed
2 |
3 | * Remember UI state when switching between landscape and veritcal views (Thanks @pangratt12345)
4 | * Add more poll intervals (Thanks @pangratt12345)
5 | * Improve poll interval drop down menu (Thanks @pangratt12345)
6 | * Check email and password input before sending request to server (Thanks @pangratt12345)
7 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/39.txt:
--------------------------------------------------------------------------------
1 | # Fix
2 |
3 | Reproducible builds for fdroid.
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/40.txt:
--------------------------------------------------------------------------------
1 | # Added
2 |
3 | * Received messages can now be marked read, moved to trash or spam from the notification
4 |
5 | # Changed
6 |
7 | * Create individual notifications for each message and group them by account email.
8 |
9 | # Fixed
10 |
11 | * Going back in the activity history should now correctly return to the last place before opening
12 | the mail application.
13 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/41.txt:
--------------------------------------------------------------------------------
1 | # Fix
2 |
3 | - Persist notification ids to avoid notification mix up.
4 | - Reduce number of database connections.
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/42.txt:
--------------------------------------------------------------------------------
1 | # Changed
2 |
3 | - Replace go-srp with proton-srp
4 | - Update dependencies
5 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | Small application to notify you when you receive an email in your email account.
2 | This may be useful for cases where you only wish to be notified when your email
3 | account has a new message and the default notification mechanism does not work
4 | (e.g: Android without Google Play Services) or do not wish to have the web
5 | interface/email client open at all times.
6 |
7 | Supported Backends
8 |
9 | The application structure has been made backend agnostics, so it should be possible to add
10 | different providers in the future. Currently, the following email providers are supported:
11 |
12 | * Proton Mail
13 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot-01.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot-02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot-02.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot-03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot-03.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot-04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot-04.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot-05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot-05.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | Email Notification without Google Play Services
2 |
--------------------------------------------------------------------------------
/scripts/build-podman-exec.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -eou pipefail
4 |
5 | OUTPUT="/opt/artifacts/app-signed.apk"
6 | ALIGNED="/tmp/app-aligned.apk"
7 |
8 | source ~/.cargo/env
9 |
10 | echo "Cloning repo"
11 | git clone "https://github.com/LeanderBB/you-have-mail.git" /opt/project
12 |
13 | cd /opt/project/you-have-mail-android
14 |
15 | # Build Project
16 | ./gradlew --no-daemon assembleRelease
17 |
18 | # Align zip
19 | echo "Aligning apk"
20 | $ANDROID_HOME/build-tools/$ANDROID_BUILD_TOOLS_VERSION/zipalign -v 4 \
21 | app/build/outputs/apk/release/app-release-unsigned.apk $ALIGNED
22 |
23 | # Sign apk
24 | echo "Signing apk"
25 | echo $KEY_STORE_PWD | $ANDROID_HOME/build-tools/$ANDROID_BUILD_TOOLS_VERSION/apksigner sign \
26 | --ks /opt/keystore \
27 | --in $ALIGNED \
28 | --out $OUTPUT
29 |
30 | # Verify
31 | echo "Verifying apk"
32 | $ANDROID_HOME/build-tools/$ANDROID_BUILD_TOOLS_VERSION/apksigner verify $OUTPUT
33 |
34 |
--------------------------------------------------------------------------------
/scripts/build-podman.sh:
--------------------------------------------------------------------------------
1 | #/bin/bash
2 | #
3 | set -eou pipefail
4 |
5 | mkdir -p artifacts
6 |
7 | scripts/podman-do.sh "/opt/scripts/build-podman-exec.sh"
8 |
9 |
--------------------------------------------------------------------------------
/scripts/podman-do.sh:
--------------------------------------------------------------------------------
1 | #/bin/bash
2 | #
3 | set -eou pipefail
4 |
5 | podman run --rm \
6 | --volume $PWD/scripts:/opt/scripts:O \
7 | --volume $PWD/artifacts:/opt/artifacts:Z \
8 | --secret android_keystore,type=mount,target=/opt/keystore \
9 | --secret android_key_pwd,type=env,target=KEY_STORE_PWD \
10 | -i -t yhm $@
11 |
12 |
--------------------------------------------------------------------------------
/you-have-mail-android/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/you-have-mail-android/README.md:
--------------------------------------------------------------------------------
1 | # You Have Mail Android
2 |
3 | Here you can find the android application that uses the You Have Mail shared service code.
4 |
5 | ## Architecture
6 |
7 | This app has been specifically designed to work in cases where Google Play services are not available. The App launches
8 | a Foreground Service that polls the accounts every 15 seconds. Once an account reports a new message has arrived it
9 | will create a notification.
10 |
11 | When the App knows about which backend maps to which Android application, it will try to launch that app if the user
12 | clicks on the application.
13 |
14 | ## Compatibility
15 |
16 | The Application is only available for x86_64 and Aarch64 and is has API 29 as minimum requirements. It has been tested
17 | on a Pixel 3a running the last compatible version of [Graphene OS](https://grapheneos.org/).
18 |
19 | ## Security
20 |
21 | Session tokens are stored using the `EncryptedSharedPreferences` API.
22 |
23 | ## Building and Installing using Android Studio
24 |
25 | This application is built in Android Studio. It uses Rust programming language.
26 | In order to build and run this application in Android Studio install Rust in your environment.
27 | Below commands are for Linux Debian/Ubuntu:
28 | ```
29 | sudo apt-get install -y python-is-python3 golang-go;
30 | # Install Rust using official method
31 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
32 | rustup default stable;
33 | rustup target add armv7-linux-androideabi;
34 | rustup target add aarch64-linux-android;
35 | rustup target add x86_64-linux-android;
36 | ```
37 | Alternatively use suitable docker image with Rust preinstalled instead of manual installation above.
38 |
39 |
--------------------------------------------------------------------------------
/you-have-mail-android/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/you-have-mail-android/app/build.gradle:
--------------------------------------------------------------------------------
1 | // NB: Android Studio can't find the imports; this does not affect the
2 | // actual build since Gradle can find them just fine.
3 | import com.android.tools.profgen.ArtProfileKt
4 | import com.android.tools.profgen.ArtProfileSerializer
5 | import com.android.tools.profgen.DexFile
6 | import org.gradle.internal.os.OperatingSystem
7 |
8 | plugins {
9 | id 'com.android.application'
10 | id 'org.jetbrains.kotlin.android'
11 | id 'kotlin-parcelize'
12 | id "org.mozilla.rust-android-gradle.rust-android" version "0.9.5"
13 | }
14 |
15 | android {
16 | namespace 'dev.lbeernaert.youhavemail'
17 | compileSdk 34
18 |
19 | defaultConfig {
20 | applicationId "dev.lbeernaert.youhavemail"
21 | minSdk 26
22 | targetSdk 33
23 | versionCode 42
24 | versionName "0.20.0"
25 |
26 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
27 | vectorDrawables {
28 | useSupportLibrary true
29 | }
30 | }
31 |
32 | buildTypes {
33 | release {
34 | minifyEnabled false
35 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
36 | }
37 | }
38 | compileOptions {
39 | sourceCompatibility JavaVersion.VERSION_11
40 | targetCompatibility JavaVersion.VERSION_11
41 | }
42 | kotlinOptions {
43 | jvmTarget = '11'
44 | }
45 | buildFeatures {
46 | compose true
47 | }
48 | composeOptions {
49 | kotlinCompilerExtensionVersion '1.4.0'
50 | }
51 | packagingOptions {
52 | resources {
53 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
54 | }
55 | }
56 | ndkVersion "25.2.9519653"
57 |
58 | defaultConfig {
59 | ndk {
60 | //noinspection ChromeOsAbiSupport
61 | abiFilters 'arm64-v8a', 'x86_64', 'armeabi-v7a'
62 | }
63 | }
64 |
65 | // https://gitlab.com/fdroid/fdroiddata/-/issues/3330#note_2189915200
66 | dependenciesInfo {
67 | // Disables dependency metadata when building APKs.
68 | includeInApk = false
69 | // Disables dependency metadata when building Android App Bundles.
70 | includeInBundle = false
71 | }
72 | }
73 |
74 | dependencies {
75 | // WorkManager dependency
76 | implementation "androidx.work:work-runtime-ktx:$versions_work"
77 |
78 | implementation 'androidx.core:core-ktx:1.13.1'
79 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.4'
80 | implementation 'androidx.activity:activity-compose:1.9.1'
81 | implementation "androidx.compose.ui:ui:$compose_ui_version"
82 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
83 | implementation 'androidx.compose.material:material:1.6.8'
84 | implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha06'
85 | implementation 'androidx.appcompat:appcompat:1.7.0'
86 | implementation 'com.google.android.material:material:1.12.0'
87 | testImplementation 'junit:junit:4.13.2'
88 | androidTestImplementation 'androidx.test.ext:junit:1.2.1'
89 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
90 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
91 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
92 | debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
93 | implementation 'androidx.navigation:navigation-compose:2.8.9'
94 | implementation "androidx.compose.material:material-icons-extended:$compose_ui_version"
95 |
96 | // Required for JNI
97 | implementation 'net.java.dev.jna:jna:5.14.0@aar'
98 | }
99 |
100 | apply plugin: 'org.mozilla.rust-android-gradle.rust-android'
101 |
102 | cargo {
103 | module = "../../you-have-mail-mobile"
104 | libname = "youhavemail"
105 | targets = ["x86_64", "arm64", "arm"]
106 | targetIncludes = ["libyouhavemail.so"]
107 | targetDirectory = "/tmp/build-yhm"
108 | profile = "release"
109 | prebuiltToolchains = true
110 | rustupChannel = "1.85.0"
111 | apiLevel = 26
112 |
113 | // Ensure paths are consistent for reproducible builds.
114 | exec { spec, _ ->
115 | var home = "$System.env.HOME"
116 | var pwd = "$System.env.PWD"
117 | spec.environment("RUSTFLAGS", "--remap-path-prefix=${home}=/remap --remap-path-prefix=${pwd}=/remap --remap-path-prefix=${pwd}/you-have-mail=/remap")
118 | spec.environment("CARGO_TARGET_DIR", "/tmp/build-yhm")
119 | }
120 |
121 | extraCargoBuildArguments = ['--locked']
122 | }
123 |
124 | task bindingBuild(type: Exec) {
125 | workingDir "../../you-have-mail-mobile"
126 | commandLine "cargo", "build"
127 | }
128 |
129 | var ext = "so"
130 | if (OperatingSystem.current().isMacOsX()) {
131 | ext = "dylib"
132 | }
133 |
134 | task genBindings(type: Exec) {
135 | workingDir "../../you-have-mail-mobile"
136 | commandLine "cargo", "run", "--bin", "uniffi-bindgen", "--", "generate", "--library",
137 | "target/debug/libyouhavemail.$ext",
138 | "--language", "kotlin", "--config", "uniffi.toml",
139 | "--out-dir", "../you-have-mail-android/app/src/main/java"
140 | }
141 |
142 | tasks.whenTaskAdded { task ->
143 | // Require cargo to be run before copying native libraries.
144 | if ((task.name == 'mergeDebugJniLibFolders' || task.name == 'mergeReleaseJniLibFolders')) {
145 | task.dependsOn 'cargoBuild'
146 | }
147 |
148 | if (task.name == 'preBuild') {
149 | task.dependsOn genBindings
150 | }
151 |
152 | if (task.name == 'genBindings') {
153 | task.dependsOn 'cargoBuild'
154 | }
155 |
156 | if (task.name == 'compileDebugKotlin') {
157 | task.dependsOn 'genBindings'
158 | }
159 | }
160 |
161 | afterEvaluate {
162 |
163 | // The `cargoBuild` task isn't available until after evaluation.
164 | android.applicationVariants.all { variant ->
165 | def productFlavor = ""
166 | variant.productFlavors.each {
167 | productFlavor += "${it.name.capitalize()}"
168 | }
169 | def buildType = "${variant.buildType.name.capitalize()}"
170 | tasks["generate${productFlavor}${buildType}Assets"].dependsOn(tasks["cargoBuild"])
171 | }
172 | }
173 |
174 | // Sort .profm files for reproducible builds. Taken from
175 | // https://gist.github.com/obfusk/eb82a810ed6aad266dab19977b18cee6
176 | project.afterEvaluate {
177 | tasks.each { task ->
178 | if (task.name.startsWith("compile") && task.name.endsWith("ReleaseArtProfile")) {
179 | task.doLast {
180 | outputs.files.each { file ->
181 | if (file.name.endsWith(".profm")) {
182 | println("Sorting ${file} ...")
183 | def version = ArtProfileSerializer.valueOf("METADATA_0_0_2")
184 | def profile = ArtProfileKt.ArtProfile(file)
185 | def keys = new ArrayList(profile.profileData.keySet())
186 | def sortedData = new LinkedHashMap()
187 | Collections.sort keys, new DexFile.Companion()
188 | keys.each { key -> sortedData[key] = profile.profileData[key] }
189 | new FileOutputStream(file).with {
190 | write(version.magicBytes$profgen)
191 | write(version.versionBytes$profgen)
192 | version.write$profgen(it, sortedData, "")
193 | }
194 | }
195 | }
196 | }
197 | }
198 | }
199 | }
200 |
201 | afterEvaluate {
202 | genBindings.dependsOn(bindingBuild, cargoBuild)
203 | android.applicationVariants.all { variant ->
204 | variant.javaCompiler.dependsOn(genBindings)
205 | }
206 | }
--------------------------------------------------------------------------------
/you-have-mail-android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/androidTest/java/dev/lbeernaert/youhavemail/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("dev.lbeernaert.youhavemail", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
27 |
28 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
48 |
49 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/.gitignore:
--------------------------------------------------------------------------------
1 | youhavemail.kt
2 |
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/OpenAppActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail
2 |
3 | import android.app.Activity
4 | import android.app.PendingIntent
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.os.Bundle
8 | import android.util.Log
9 | import android.widget.Toast
10 | import dev.lbeernaert.youhavemail.app.NOTIFICATION_LOG_TAG
11 | import dev.lbeernaert.youhavemail.app.NOTIFICATION_STATE
12 | import dev.lbeernaert.youhavemail.app.NotificationActionClicked
13 | import dev.lbeernaert.youhavemail.app.NotificationIntentAppNameKey
14 | import dev.lbeernaert.youhavemail.app.NotificationIntentBackendKey
15 | import dev.lbeernaert.youhavemail.app.NotificationIntentEmailKey
16 | import dev.lbeernaert.youhavemail.app.getAppNameForBackend
17 | import dev.lbeernaert.youhavemail.app.getOrCreateEncryptionKey
18 | import dev.lbeernaert.youhavemail.app.newRequestCode
19 |
20 | class OpenAppActivity : Activity() {
21 | override fun onCreate(savedInstanceState: Bundle?) {
22 | super.onCreate(savedInstanceState)
23 | val action = intent.action ?: return
24 |
25 | finish()
26 |
27 | if (action == NotificationActionClicked) {
28 | val backend = intent.getStringExtra(NotificationIntentBackendKey)!!
29 | val email = intent.getStringExtra(NotificationIntentEmailKey)!!
30 | val appName = intent.getStringExtra(NotificationIntentAppNameKey)!!
31 |
32 |
33 | // Launch the app for this backend
34 | Log.d(activityLogTag, "Receive click request for '$email' backend='$backend'")
35 | try {
36 | // Dismiss group notifications and children as they are not auto cleared
37 | // in this case.
38 | NOTIFICATION_STATE.dismissGroupNotification(this, email, true)
39 |
40 | Log.d(activityLogTag, "Attempting to launch $appName")
41 | val appIntent =
42 | packageManager.getLaunchIntentForPackage(appName)
43 | if (appIntent != null) {
44 | appIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
45 | appIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
46 | appIntent.addFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
47 | appIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
48 | startActivity(appIntent)
49 | } else {
50 | Log.e(activityLogTag, "Could not find package $appName")
51 | Toast.makeText(this, "Could not find package $appName", Toast.LENGTH_LONG)
52 | .show()
53 | }
54 | } catch (e: Exception) {
55 | Log.e(activityLogTag, "Failed to launch $appName for backend $backend: $e")
56 | yhmLogError("Failed to launch $appName for backend $backend: $e")
57 | Toast.makeText(
58 | this,
59 | "Failed to launch $appName for backed $backend",
60 | Toast.LENGTH_LONG
61 | )
62 | .show()
63 | }
64 | }
65 | }
66 |
67 | companion object {
68 | fun newIntent(
69 | context: Context,
70 | email: String,
71 | backend: String
72 | ): PendingIntent? {
73 | val appName = getAppNameForBackend(backend)
74 | return if (appName != null) {
75 | Intent(context, OpenAppActivity::class.java).let { intent ->
76 | intent.action = NotificationActionClicked
77 | intent.putExtra(
78 | NotificationIntentEmailKey, email
79 | )
80 | intent.putExtra(
81 | NotificationIntentBackendKey, backend
82 | )
83 | intent.putExtra(
84 | NotificationIntentAppNameKey,
85 | appName
86 | )
87 | intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
88 | intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
89 | PendingIntent.getActivity(
90 | context,
91 | newRequestCode(),
92 | intent,
93 | PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
94 | )
95 | }
96 | } else {
97 | Log.d(
98 | NOTIFICATION_LOG_TAG,
99 | "No app found for backed '$backend'. No notification action"
100 | )
101 | null
102 | }
103 | }
104 | }
105 | }
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/app/ActionReceivers.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.app
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.util.Log
7 | import dev.lbeernaert.youhavemail.R
8 | import dev.lbeernaert.youhavemail.Yhm
9 | import dev.lbeernaert.youhavemail.yhmLogError
10 | import dev.lbeernaert.youhavemail.yhmLogInfo
11 |
12 |
13 | private const val NotificationIDKey = "NotificationID"
14 | private const val TAG = "Receivers"
15 |
16 | /**
17 | * Base class receiver for actions which can be taken on a notification.
18 | */
19 | open class ActionReceiver(
20 | private val name: String,
21 | private val descriptionSuccess: Int,
22 | private val descriptionFail: Int
23 | ) :
24 | BroadcastReceiver() {
25 |
26 | override fun onReceive(context: Context, intent: Intent) {
27 | val email = intent.getStringExtra(NotificationIntentEmailKey)
28 | val action = intent.getStringExtra(NotificationIntentActionKey)
29 | val backend = intent.getStringExtra(NotificationIntentBackendKey)
30 | if (email == null || action == null || backend == null) {
31 | yhmLogError("Received $name broadcast, but email, backend or action was not set")
32 | Log.e(TAG, "Received $name broadcast, but email, backend or action was not set")
33 | return
34 | }
35 | val notificationID = intent.getIntExtra(NotificationIDKey, 0)
36 | if (notificationID != 0) {
37 | NOTIFICATION_STATE.dismissNotification(context, email, backend, notificationID)
38 | }
39 | yhmLogInfo("Received $name broadcast for $email")
40 |
41 | try {
42 | ActionWorker.queue(context, email, action, descriptionSuccess, descriptionFail)
43 | } catch (e: Exception) {
44 | yhmLogError("Failed to queue action for $email: $e")
45 | createAndDisplayServiceErrorNotification(
46 | context,
47 | "Failed to queue action for $email: $e"
48 | )
49 | }
50 | }
51 |
52 | companion object {
53 | fun fillIntentArgs(
54 | intent: Intent,
55 | notificationID: Int,
56 | email: String,
57 | backend: String,
58 | action: String,
59 | ): Intent {
60 | return intent.putExtra(NotificationIntentEmailKey, email)
61 | .putExtra(NotificationIntentActionKey, action)
62 | .putExtra(NotificationIDKey, notificationID)
63 | .putExtra(NotificationIntentBackendKey, backend)
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * Broadcast when the user clicks the `Mark Read` notification action.
70 | */
71 | class MarkReadReceiver : ActionReceiver(
72 | name = "MarkRead",
73 | R.string.msg_mark_read_success,
74 | R.string.msg_mark_read_fail
75 | ) {
76 | companion object {
77 | fun newIntent(
78 | context: Context,
79 | notificationID: Int,
80 | email: String,
81 | backend: String,
82 | action: String,
83 | ): Intent {
84 | return Intent(context, MarkReadReceiver::class.java).let {
85 | fillIntentArgs(it, notificationID, email, backend, action)
86 | // Need to set unique action to prevent caching
87 | .setAction("MarkRead-$notificationID-${System.currentTimeMillis()}")
88 | }
89 | }
90 | }
91 | }
92 |
93 | /**
94 | * Broadcast when the user clicks the `Trash` notification action.
95 | */
96 | class MoveToTrashReceiver : ActionReceiver(
97 | name = "MoveToTrash",
98 | R.string.msg_trash_success,
99 | R.string.msg_trash_fail
100 | ) {
101 | companion object {
102 | fun newIntent(
103 | context: Context,
104 | notificationID: Int,
105 | email: String,
106 | backend: String,
107 | action: String
108 | ): Intent {
109 | return Intent(context, MoveToTrashReceiver::class.java).let {
110 | fillIntentArgs(it, notificationID, email, backend, action)
111 | // Need to set unique action to prevent caching
112 | .setAction("MoveToTrash-$notificationID-${System.currentTimeMillis()}")
113 | }
114 | }
115 | }
116 | }
117 |
118 | /**
119 | * Broadcast when the user clicks the `Spam` notification action.
120 | */
121 | class MoveToSpamReceiver : ActionReceiver(
122 | "MoveToSpam",
123 | R.string.msg_spam_success,
124 | R.string.msg_spam_fail
125 | ) {
126 | companion object {
127 | fun newIntent(
128 | context: Context,
129 | notificationID: Int,
130 | email: String,
131 | backend: String,
132 | action: String
133 | ): Intent {
134 | return Intent(context, MoveToSpamReceiver::class.java).let {
135 | fillIntentArgs(it, notificationID, email, backend, action)
136 | // Need to set unique action to prevent caching
137 | .setAction("MoveToSpam-$notificationID-${System.currentTimeMillis()}")
138 | }
139 | }
140 | }
141 | }
142 |
143 | /**
144 | * Receiver to dismiss group notifications
145 | */
146 | class DismissGroupNotificationReceiver : BroadcastReceiver() {
147 | override fun onReceive(context: Context, intent: Intent) {
148 | val email = intent.getStringExtra(NotificationIntentEmailKey)
149 | if (email != null) {
150 | Log.i(TAG, "Dismissing Group Notification for $email")
151 | // When dismissing a group, all children are also dismissed.
152 | NOTIFICATION_STATE.dismissGroupNotification(context, email, false)
153 | }
154 | }
155 |
156 | companion object {
157 | fun newIntent(
158 | context: Context,
159 | email: String,
160 | ): Intent {
161 | return Intent(context, DismissGroupNotificationReceiver::class.java).putExtra(
162 | NotificationIntentEmailKey,
163 | email
164 | )
165 | // Need to set unique action to prevent caching
166 | .setAction("DismissGroupNotification-${System.currentTimeMillis()}")
167 | }
168 | }
169 | }
170 |
171 | /**
172 | * Receiver to dismiss a message notifications
173 | */
174 | class DismissMessageNotificationReceiver : BroadcastReceiver() {
175 | override fun onReceive(context: Context, intent: Intent) {
176 | val email = intent.getStringExtra(NotificationIntentEmailKey)
177 | val backend = intent.getStringExtra(NotificationIntentBackendKey)
178 | val id = intent.getIntExtra(NotificationIDKey, 0)
179 | if (email != null && id != 0 && backend != null) {
180 | Log.i(TAG, "Dismissing Message Notification for $email id=$id")
181 | NOTIFICATION_STATE.dismissNotification(context, email, backend, id)
182 | }
183 | }
184 |
185 | companion object {
186 | fun newIntent(
187 | context: Context,
188 | email: String,
189 | backend: String,
190 | notificationID: Int,
191 | ): Intent {
192 | return Intent(context, DismissMessageNotificationReceiver::class.java).putExtra(
193 | NotificationIntentEmailKey,
194 | email
195 | ).putExtra(NotificationIDKey, notificationID)
196 | .putExtra(NotificationIntentBackendKey, backend)
197 | // Need to set unique action to prevent caching
198 | .setAction("DismissMessageNotification-$notificationID-${System.currentTimeMillis()}")
199 | }
200 | }
201 | }
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/app/ActionWorker.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.app
2 |
3 | import android.content.Context
4 | import android.os.Handler
5 | import android.util.Log
6 | import android.widget.Toast
7 | import androidx.work.Constraints
8 | import androidx.work.Data
9 | import androidx.work.NetworkType
10 | import androidx.work.OneTimeWorkRequest
11 | import androidx.work.WorkManager
12 | import androidx.work.Worker
13 | import androidx.work.WorkerParameters
14 | import dev.lbeernaert.youhavemail.R
15 | import dev.lbeernaert.youhavemail.Yhm
16 |
17 | private const val TAG = "ActionWorker"
18 | private const val EmailKey = "Email"
19 | private const val ActionKey = "Key"
20 | private const val ActionDescSuccessKey = "ActionDescSuccess"
21 | private const val ActionDescFailKey = "ActionDescFailure"
22 |
23 | /**
24 | * Worker which execute an action
25 | */
26 | class ActionWorker(ctx: Context, params: WorkerParameters) :
27 | Worker(ctx, params) {
28 |
29 | override fun doWork(): Result {
30 | val email = this.inputData.getString(EmailKey)
31 | val action = this.inputData.getString(ActionKey)
32 | if (email == null || action == null) {
33 | return Result.failure()
34 | }
35 | val actionDescSuccess =
36 | this.inputData.getInt(ActionDescSuccessKey, R.string.msg_action_success)
37 | val actionDescFail =
38 | this.inputData.getInt(ActionDescFailKey, R.string.msg_action_fail)
39 | Log.i(TAG, "email=$email action=$action")
40 | val handler = Handler(applicationContext.mainLooper);
41 | return try {
42 | executeAction(applicationContext, email, action)
43 | handler.postDelayed({
44 | Toast.makeText(
45 | applicationContext,
46 | actionDescSuccess,
47 | Toast.LENGTH_LONG
48 | )
49 | .show()
50 | }, 1000)
51 | Result.success()
52 | } catch (e: Exception) {
53 | Log.e(TAG, "Failed to apply action: $e")
54 | handler.postDelayed({
55 | Toast.makeText(
56 | applicationContext,
57 | actionDescFail,
58 | Toast.LENGTH_LONG
59 | )
60 | .show()
61 | }, 1000)
62 | Result.failure()
63 | }
64 | }
65 |
66 | companion object {
67 | fun queue(
68 | context: Context,
69 | email: String,
70 | action: String,
71 | successString: Int,
72 | failureString: Int
73 | ) {
74 | val constraint =
75 | Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
76 | val wm = WorkManager.getInstance(context)
77 |
78 | val inputData =
79 | Data.Builder().putString(EmailKey, email)
80 | .putString(ActionKey, action)
81 | .putInt(ActionDescSuccessKey, successString)
82 | .putInt(ActionDescFailKey, failureString).build()
83 |
84 | val work = OneTimeWorkRequest.Builder(ActionWorker::class.java)
85 | .setInputData(inputData).setConstraints(constraint)
86 | .build()
87 | wm.enqueue(work)
88 | }
89 | }
90 | }
91 |
92 | private fun executeAction(context: Context, email: String, action: String) {
93 | YhmInstance.get(context).yhm.applyAction(email, action)
94 | }
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/app/Backends.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.app
2 |
3 | const val PROTON_BACKEND_NAME = "Proton Mail"
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/app/LoginSequence.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.app
2 |
3 | import androidx.navigation.NavHostController
4 | import dev.lbeernaert.youhavemail.ProtonLoginException
5 | import dev.lbeernaert.youhavemail.ProtonLoginSequence
6 | import dev.lbeernaert.youhavemail.Proxy
7 | import dev.lbeernaert.youhavemail.Yhm
8 | import dev.lbeernaert.youhavemail.screens.Routes
9 |
10 | /**
11 | * Minor abstraction over actual login process for each backend. The backend does not need
12 | * to implement all these methods and backend specific methods can be added as needed.
13 | */
14 | interface LoginSequence {
15 | /**
16 | * Perform login and navigate to the next screen.
17 | */
18 | fun login(email: String, password: String)
19 |
20 | /**
21 | * Submit totp code and navigate to the next screen.
22 | */
23 | fun totp(code: String)
24 |
25 | /**
26 | * Proton specific captcha handler.
27 | *
28 | * Submit the token and navigate to the next screen.
29 | */
30 | fun protonCaptcha(token: String)
31 |
32 |
33 | /**
34 | * Proton specific, get captcha html.
35 | */
36 | fun protonCaptchaHtml(): String
37 |
38 | /**
39 | * Advance to the next stage in the login sequence.
40 | */
41 | fun next(navController: NavHostController)
42 |
43 | /**
44 | * Return the backend name for this login sequence.
45 | */
46 | fun backendName(): String
47 | }
48 |
49 | /**
50 | * Proton Login Sequence implementation
51 | */
52 | class ProtonLogin(state: State, proxy: Proxy?) : LoginSequence {
53 | private var mState = state
54 | private var mSequence = ProtonLoginSequence(proxy)
55 |
56 | // Email is recorded for auto captcha login
57 | private var mEmail: String = ""
58 |
59 | // Password is recorded for auto captcha login
60 | private var mPassword: String = ""
61 | private var mHumanVerificationData: String? = null
62 | private var mCaptchaHtml: String? = null
63 |
64 | override fun login(email: String, password: String) {
65 | mEmail = email
66 | mPassword = password
67 | try {
68 | mSequence.login(email, password, mHumanVerificationData)
69 | } catch (e: ProtonLoginException) {
70 | handleLoginException(e)
71 | }
72 | }
73 |
74 | override fun totp(code: String) {
75 | try {
76 | mSequence.submitTotp(code)
77 | } catch (e: ProtonLoginException) {
78 | handleLoginException(e)
79 | }
80 | }
81 |
82 | override fun protonCaptcha(token: String) {
83 | mHumanVerificationData = token
84 | mCaptchaHtml = null
85 | login(email = mEmail, password = mPassword)
86 | }
87 |
88 | override fun protonCaptchaHtml(): String {
89 | return mCaptchaHtml.orEmpty()
90 | }
91 |
92 | override fun next(navController: NavHostController) {
93 | if (mCaptchaHtml != null) {
94 | navController.navigate(Routes.ProtonCaptcha.route)
95 | } else if (mSequence.isAwaitingTotp()) {
96 | navController.navigate(Routes.TOTP.route)
97 | } else if (mSequence.isLoggedIn()) {
98 | mSequence.createAccount(mState.yhm())
99 | navController.popBackStack(Routes.Main.route, false)
100 | }
101 | }
102 |
103 | override fun backendName(): String {
104 | return PROTON_BACKEND_NAME
105 | }
106 |
107 | private fun handleLoginException(e: ProtonLoginException) {
108 | when (e) {
109 | is ProtonLoginException.HumanVerificationRequired -> {
110 | // Avoid Loop
111 | if (mHumanVerificationData != null) {
112 | throw RuntimeException("Captcha Request Loop")
113 | }
114 | mCaptchaHtml = mSequence.captcha(e.v1.token)
115 | }
116 |
117 | else -> {
118 | throw e
119 | }
120 | }
121 | }
122 | }
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/app/Logs.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.app
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import android.util.Log
6 | import android.widget.Toast
7 | import dev.lbeernaert.youhavemail.R
8 | import java.io.BufferedOutputStream
9 | import java.util.zip.ZipEntry
10 | import java.util.zip.ZipOutputStream
11 |
12 | const val logsExportLogTag = "logs-export"
13 | const val LOG_EXPORT_REQUEST = 1024
14 |
15 | fun exportLogs(context: Context, outputFile: Uri) {
16 | try {
17 | val inputDirectory = getLogPath(context)
18 | Log.d(logsExportLogTag, "Preparing log zip file: $outputFile")
19 | ZipOutputStream(
20 | BufferedOutputStream(
21 | context.contentResolver.openOutputStream(outputFile)
22 | )
23 | ).use { zos ->
24 | inputDirectory.walkTopDown().forEach { file ->
25 | val zipFileName =
26 | file.absolutePath.removePrefix(inputDirectory.absolutePath).removePrefix("/")
27 | val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}")
28 | zos.putNextEntry(entry)
29 | if (file.isFile) {
30 | Log.d(logsExportLogTag, "Adding file to zip: $zipFileName - $file")
31 | file.inputStream().use { fis -> fis.copyTo(zos) }
32 | }
33 | }
34 | }
35 |
36 | Toast.makeText(
37 | context,
38 | context.getString(R.string.export_logs_success),
39 | Toast.LENGTH_LONG
40 | ).show()
41 | } catch (e: Exception) {
42 | Log.e(logsExportLogTag, "Failed to export logs:$e")
43 | Toast.makeText(
44 | context,
45 | context.getString(R.string.export_logs_failed, e),
46 | Toast.LENGTH_LONG
47 | ).show()
48 | }
49 |
50 | }
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/app/PollWorker.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.app
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.util.Log
6 | import androidx.localbroadcastmanager.content.LocalBroadcastManager
7 | import androidx.work.Constraints
8 | import androidx.work.Data
9 | import androidx.work.ExistingPeriodicWorkPolicy
10 | import androidx.work.ExistingWorkPolicy
11 | import androidx.work.NetworkType
12 | import androidx.work.OneTimeWorkRequest
13 | import androidx.work.PeriodicWorkRequest
14 | import androidx.work.WorkManager
15 | import androidx.work.Worker
16 | import androidx.work.WorkerParameters
17 | import androidx.work.hasKeyWithValueOfType
18 | import dev.lbeernaert.youhavemail.Event
19 | import dev.lbeernaert.youhavemail.Yhm
20 | import dev.lbeernaert.youhavemail.YhmException
21 | import dev.lbeernaert.youhavemail.initLog
22 | import dev.lbeernaert.youhavemail.yhmLogError
23 | import dev.lbeernaert.youhavemail.yhmLogInfo
24 | import java.util.concurrent.TimeUnit
25 |
26 | private const val TAG = "PollWorker"
27 | private const val TAG_ONE_SHOT = "OneShotWorker"
28 | private const val POLL_WORKER_JOB_NAME = "PollWorker"
29 | private const val ONE_SHOT_WORKER_JOB_NAME = "OneShotWorker"
30 | const val POLL_INTENT = "POLL_INTENT"
31 | const val POLL_ERROR_KEY = "POLL_ERROR"
32 |
33 |
34 | /**
35 | * Periodic Worker which polls at longer intervals.
36 | */
37 | class PollWorker(ctx: Context, params: WorkerParameters) :
38 | Worker(ctx, params) {
39 |
40 | override fun doWork(): Result {
41 | return try {
42 | poll(applicationContext)
43 | Result.success()
44 | } catch (e: Exception) {
45 | Log.e(TAG, "Failed to send local broadcast: $e")
46 | Result.failure()
47 | }
48 | }
49 | }
50 |
51 | /**
52 | * One time poll worker for shorter intervals.
53 | *
54 | * If no input data is specified for the interval, subsequent launches are not repeated.
55 | */
56 | class OneTimePollWorker(ctx: Context, params: WorkerParameters) :
57 | Worker(ctx, params) {
58 |
59 | override fun doWork(): Result {
60 | try {
61 | poll(applicationContext)
62 | } catch (e: Exception) {
63 | Log.e(TAG, "Failed to poll: $e")
64 | yhmLogError("Unhandled exception: $e")
65 | return Result.failure()
66 | }
67 |
68 | try {
69 | if (inputData.hasKeyWithValueOfType("INTERVAL")) {
70 | val interval = inputData.getLong("INTERVAL", 15)
71 | registerWorker(applicationContext, interval, false)
72 | }
73 | } catch (e: Exception) {
74 | yhmLogError("Failed to re-register one time poll worker: $e")
75 | createServiceErrorNotification(applicationContext, "Failed to create worker: $e")
76 | }
77 |
78 | return Result.success()
79 | }
80 | }
81 |
82 | /**
83 | * Poll all accounts.
84 | */
85 | private fun poll(context: Context) {
86 | val yhm: Yhm
87 | try {
88 | yhm = YhmInstance.get(context).yhm
89 | } catch (e: YhmException) {
90 | createServiceErrorNotification(context, "Failed to Create Yhm", e)
91 | return
92 | }
93 |
94 | var error: String? = null
95 | try {
96 | yhm.poll()
97 | try {
98 | val events = yhm.lastEvents()
99 | for (event in events) {
100 | when (event) {
101 | is Event.Email -> {
102 | for (email in event.emails) {
103 | NOTIFICATION_STATE.onNewEmail(
104 | context,
105 | event.email,
106 | event.backend,
107 | email,
108 | )
109 | }
110 | }
111 |
112 | is Event.Error -> {
113 | NOTIFICATION_STATE.onError(context, event.v1, event.v2)
114 | }
115 |
116 | is Event.LoggedOut -> {
117 | NOTIFICATION_STATE.onLoggedOut(
118 | context,
119 | event.v1,
120 | )
121 | }
122 |
123 | is Event.Offline -> {
124 | // Do nothing.
125 | }
126 | }
127 | }
128 | } catch (e: Exception) {
129 | createServiceErrorNotification(context, "Failed to retrieve events: $e")
130 | }
131 | } catch (e: YhmException) {
132 | error = e.toString()
133 | createServiceErrorNotification(context, error)
134 | }
135 |
136 |
137 | // While it would be preferable to have a watcher into the database detect, these changes and
138 | // since currently there exists no such thing for rust, we simply broad cast the success of
139 | // this work and let the main activity handle the notification state.
140 |
141 | try {
142 | val localIntent = Intent(POLL_INTENT)
143 | localIntent.putExtra(POLL_ERROR_KEY, error)
144 | LocalBroadcastManager.getInstance(context).sendBroadcast(localIntent)
145 | } catch (e: Exception) {
146 | yhmLogError("Failed to publish broadcast: $e")
147 | createServiceErrorNotification(context, "Failed to publish broadcast: $e")
148 | }
149 | }
150 |
151 | /**
152 | * Get worker constraints
153 | */
154 | private fun constraints(): Constraints {
155 | return Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
156 | }
157 |
158 | fun registerWorker(ctx: Context, minutes: Long, cancel: Boolean) {
159 | initLog(getLogPath(ctx).path)
160 | val inputData = Data.Builder().putLong("INTERVAL", minutes).build()
161 | val constraints = constraints()
162 | val wm = WorkManager.getInstance(ctx)
163 |
164 | if (cancel) {
165 | wm.cancelAllWorkByTag(TAG)
166 | }
167 |
168 | if (minutes >= 15) {
169 | Log.d(TAG, "Registering Periodic work with $minutes minutes interval")
170 |
171 | val work =
172 | PeriodicWorkRequest.Builder(PollWorker::class.java, minutes, TimeUnit.MINUTES)
173 | .addTag(TAG)
174 | .setInputData(inputData)
175 | .setConstraints(constraints)
176 | .build()
177 | wm.enqueueUniquePeriodicWork(
178 | POLL_WORKER_JOB_NAME,
179 | ExistingPeriodicWorkPolicy.KEEP,
180 | work
181 | )
182 | } else {
183 | Log.d(TAG, "Registering One Time work with $minutes minutes interval")
184 | val work = OneTimeWorkRequest.Builder(OneTimePollWorker::class.java).addTag(TAG)
185 | .setInputData(inputData).setConstraints(constraints)
186 | .setInitialDelay(minutes, TimeUnit.MINUTES)
187 | .build()
188 |
189 | wm.enqueueUniqueWork(
190 | POLL_WORKER_JOB_NAME,
191 | ExistingWorkPolicy.REPLACE,
192 | work
193 | )
194 | }
195 | }
196 |
197 | /**
198 | * Register a poll job that only runs once.
199 | */
200 | fun oneshotWorker(ctx: Context) {
201 | Log.d(TAG, "Registering one time shot worker")
202 | val work = OneTimeWorkRequest.Builder(OneTimePollWorker::class.java).addTag(TAG_ONE_SHOT)
203 | .setConstraints(constraints())
204 | .build()
205 |
206 | val wm = WorkManager.getInstance(ctx)
207 | wm.enqueueUniqueWork(ONE_SHOT_WORKER_JOB_NAME, ExistingWorkPolicy.KEEP, work)
208 | }
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/app/StartReceiver.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.app
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.util.Log
7 | import dev.lbeernaert.youhavemail.Yhm
8 | import dev.lbeernaert.youhavemail.YhmException
9 |
10 | class StartReceiver : BroadcastReceiver() {
11 | override fun onReceive(context: Context, intent: Intent) {
12 | if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
13 | Log.i("BOOT", "Received boot notification")
14 |
15 | try {
16 | registerWorker(
17 | context,
18 | YhmInstance.get(context).yhm.pollInterval().toLong() / 60,
19 | false
20 | )
21 | } catch (e: YhmException) {
22 | createServiceErrorNotification(
23 | context,
24 | "Failed to Create Yhm on boot and register work",
25 | e
26 | )
27 | return
28 | }
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/app/State.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.app
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import android.util.Log
6 | import androidx.security.crypto.EncryptedSharedPreferences
7 | import androidx.security.crypto.MasterKey
8 | import dev.lbeernaert.youhavemail.Account
9 | import dev.lbeernaert.youhavemail.AccountWatcher
10 | import dev.lbeernaert.youhavemail.Backend
11 | import dev.lbeernaert.youhavemail.Event
12 | import dev.lbeernaert.youhavemail.Proxy
13 | import dev.lbeernaert.youhavemail.WatchHandle
14 | import dev.lbeernaert.youhavemail.Yhm
15 | import dev.lbeernaert.youhavemail.activityLogTag
16 | import dev.lbeernaert.youhavemail.newEncryptionKey
17 | import dev.lbeernaert.youhavemail.yhmLogError
18 | import dev.lbeernaert.youhavemail.yhmLogInfo
19 | import kotlinx.coroutines.flow.MutableStateFlow
20 | import kotlinx.coroutines.flow.StateFlow
21 | import java.io.File
22 |
23 | /*
24 | data class ServiceAccount(
25 | val email: String,
26 | val backend: String,
27 | val state: MutableStateFlow,
28 | val proxy: MutableStateFlow
29 | )
30 |
31 | class ObserverServiceState {
32 | private var mAccounts: ArrayList = ArrayList()
33 | private var mAccountsFlow: MutableStateFlow> =
34 | MutableStateFlow(ArrayList())
35 | */
36 |
37 | const val STATE_LOG_TAG = "state"
38 |
39 | // Has to be global singleton for now so that the ids are accessible for the worker
40 | // and the system. Could be moved to a shared preferences setup for persistent
41 | // changes.
42 | var NOTIFICATION_STATE = NotificationState()
43 |
44 | class State(context: Context) : AccountWatcher {
45 | private var mPollInterval = MutableStateFlow(15UL)
46 | private var mYhm: Yhm = YhmInstance.get(context).yhm
47 | private var mAccounts: MutableStateFlow>
48 | private var mOpenAccount: MutableStateFlow = MutableStateFlow(null)
49 | var mLoginSequence: LoginSequence? = null
50 | private var mWatchHandle: WatchHandle? = null
51 |
52 | init {
53 | mWatchHandle = mYhm.watchAccounts(this)
54 | mAccounts = MutableStateFlow(mYhm.accounts())
55 | val pollInterval = mYhm.pollInterval()
56 | mPollInterval.value = pollInterval
57 | registerWorker(context, pollInterval.toLong() / 60, false)
58 | }
59 |
60 |
61 | fun migrateAccounts(context: Context) {
62 | val file = getConfigFilePathV1(context)
63 | if (!file.exists()) {
64 | return
65 | }
66 | try {
67 | yhmLogInfo("Found v1 config, importing...")
68 | mYhm.importV1(file.path)
69 | yhmLogInfo("Found v1 config, importing...Done")
70 | } catch (e: Exception) {
71 | yhmLogError("Failed to migrate: $e")
72 | try {
73 | createAndDisplayServiceErrorNotification(context, "Failed to migrate accounts: $e")
74 | } catch (e: Exception) {
75 | Log.e(activityLogTag, "Failed to create exception")
76 | }
77 | } finally {
78 | try {
79 | file.delete()
80 | } catch (e: Exception) {
81 | yhmLogError("Failed to delete old config file: $e")
82 | }
83 | }
84 | }
85 |
86 | /**
87 | * Update poll interval.
88 | *
89 | */
90 | fun setPollInterval(context: Context, intervalSeconds: ULong) {
91 | Log.i(STATE_LOG_TAG, "Interval $intervalSeconds Seconds")
92 | mYhm.setPollInterval(intervalSeconds)
93 | mPollInterval.value = intervalSeconds
94 | registerWorker(context, (intervalSeconds.toLong() / 60), true)
95 | }
96 |
97 | /**
98 | * Get the current poll interval
99 | */
100 | fun getPollInterval(): StateFlow {
101 | return mPollInterval
102 | }
103 |
104 | /**
105 | * Get list of known accounts
106 | */
107 | fun accounts(): StateFlow> {
108 | return mAccounts
109 | }
110 |
111 | /**
112 | * Get list of backends.
113 | */
114 | fun backends(): List {
115 | return mYhm.backends()
116 | }
117 |
118 | /**
119 | * Open an account fo detailed inspection
120 | */
121 | fun account(email: String): Account? {
122 | return mYhm.account(email)
123 | }
124 |
125 | /**
126 | * Logout account by email.
127 | */
128 | fun logout(email: String) {
129 | mYhm.logout(email)
130 | }
131 |
132 | /**
133 | * Delete an account by email.
134 | */
135 | fun delete(email: String) {
136 | mYhm.delete(email)
137 | }
138 |
139 | fun yhm(): Yhm {
140 | return mYhm
141 | }
142 |
143 | /**
144 | * Create a new login sequence for a given backend.
145 | */
146 | fun newLoginSequence(backendName: String, proxy: Proxy?): LoginSequence {
147 | if (backendName == PROTON_BACKEND_NAME) {
148 | return ProtonLogin(this, proxy)
149 | }
150 |
151 | throw RuntimeException("Unknown backend")
152 | }
153 |
154 | /**
155 | * Unregister receiver and release resource.
156 | */
157 | fun close() {
158 | Log.i(STATE_LOG_TAG, "Closing")
159 | mAccounts.value = ArrayList()
160 | mLoginSequence = null
161 | mWatchHandle = null
162 | }
163 |
164 | override fun onAccountsUpdated(accounts: List) {
165 | if (mOpenAccount.value != null) {
166 | val accountEmail = mOpenAccount.value!!.email()
167 | mOpenAccount.value = accounts.find {
168 | it.email() == accountEmail
169 | }
170 | }
171 | mAccounts.value = accounts
172 | }
173 | }
174 |
175 | /**
176 | * Get configuration file path.
177 | */
178 | private fun getConfigFilePathV1(context: Context): File {
179 | return File(context.filesDir.canonicalPath, "config_v2")
180 | }
181 |
182 | /**
183 | * Get database path.
184 | */
185 | fun getDatabasePath(context: Context): String {
186 | return context.filesDir.canonicalPath + "/yhm.db"
187 | }
188 |
189 | /**
190 | * Get log path.
191 | */
192 | fun getLogPath(context: Context): File {
193 | return File(context.filesDir.canonicalPath, "logs")
194 | }
195 |
196 | /**
197 | * Load encryption key for the application.
198 | */
199 | fun getOrCreateEncryptionKey(context: Context): String {
200 | Log.d(STATE_LOG_TAG, "Loading Encryption key")
201 | val preferences = getSharedPreferences(context)
202 | val key = preferences.getString("KEY", null)
203 | return if (key == null) {
204 | Log.d(STATE_LOG_TAG, "No key exists, creating new key")
205 | val newKey = newEncryptionKey()
206 | preferences.edit().putString("KEY", newKey).apply()
207 | newKey
208 | } else {
209 | key
210 | }
211 | }
212 |
213 | /**
214 | * Get the encrypted shared preferences for this application.
215 | */
216 | private fun getSharedPreferences(context: Context): SharedPreferences {
217 | val masterKey = MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
218 | .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
219 | .build()
220 |
221 | return EncryptedSharedPreferences.create(
222 | context,
223 | // passing a file name to share a preferences
224 | "preferences",
225 | masterKey,
226 | EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
227 | EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
228 | )
229 | }
230 |
231 | /**
232 | * Get the application we should open based on the backend name.
233 | */
234 | fun getAppNameForBackend(backend: String): String? {
235 | return when (backend) {
236 | "Proton Mail" -> "ch.protonmail.android"
237 | "Proton Mail V-Other" -> "ch.protonmail.android"
238 | "Null Backend" -> "ch.protonmail.android"
239 |
240 | else -> null
241 | }
242 | }
243 |
244 | fun accountStatusString(account: Account): String {
245 | var statusString = ""
246 | if (account.isLoggedOut()) {
247 | statusString = "Logged Out"
248 | } else {
249 | val last_poll = account.lastPoll()
250 | if (last_poll == null) {
251 | statusString = "Not Polled"
252 | } else {
253 | val last_event = account.lastEvent()
254 | if (last_event != null) {
255 | var result = "Polled"
256 | when (last_event) {
257 | is Event.Error -> {
258 | result = "Error"
259 | }
260 |
261 | is Event.Offline -> {
262 | result = "Offline"
263 | }
264 |
265 | else -> {
266 |
267 | }
268 | }
269 | statusString = "$result (${last_poll})"
270 | }
271 | }
272 | }
273 |
274 | return statusString
275 | }
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/app/YhmInstance.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.app
2 |
3 | import android.content.Context
4 | import dev.lbeernaert.youhavemail.Yhm
5 |
6 | /**
7 | * Yhm Singleton.
8 | */
9 | class YhmInstance private constructor(var yhm: Yhm) {
10 | companion object {
11 | @Volatile
12 | private var instance: YhmInstance? = null
13 |
14 | /**
15 | * Get or create a Yhm instance.
16 | *
17 | * Throws exception on failure.
18 | */
19 | fun get(context: Context): YhmInstance {
20 | if (instance != null) {
21 | return instance!!
22 | }
23 | synchronized(this) {
24 | if (instance == null) {
25 | val key = getOrCreateEncryptionKey(context)
26 | val dbPath = getDatabasePath(context)
27 | instance = YhmInstance(Yhm(dbPath, encryptionKey = key))
28 | }
29 |
30 | return instance!!
31 | }
32 | }
33 | }
34 |
35 |
36 | }
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/components/ActionButton.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.components
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.foundation.layout.height
5 | import androidx.compose.material.Button
6 | import androidx.compose.material.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.unit.dp
10 |
11 | @Composable
12 | fun ActionButton(text: String, onClick: () -> Unit, enabled: Boolean = true) {
13 | Button(
14 | onClick = onClick,
15 | modifier = Modifier
16 | .fillMaxWidth()
17 | .height(50.dp),
18 | enabled = enabled,
19 | ) {
20 | Text(text = text)
21 | }
22 | }
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/components/AsyncScreen.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.components
2 |
3 | import android.util.Log
4 | import androidx.compose.foundation.layout.PaddingValues
5 | import androidx.compose.material.Icon
6 | import androidx.compose.material.IconButton
7 | import androidx.compose.material.Scaffold
8 | import androidx.compose.material.SnackbarDuration
9 | import androidx.compose.material.Text
10 | import androidx.compose.material.TopAppBar
11 | import androidx.compose.material.icons.Icons
12 | import androidx.compose.material.icons.automirrored.filled.ArrowBack
13 | import androidx.compose.material.rememberScaffoldState
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.mutableStateOf
16 | import androidx.compose.runtime.remember
17 | import androidx.compose.runtime.rememberCoroutineScope
18 | import kotlinx.coroutines.launch
19 |
20 | const val asyncScreenLogTag = "async"
21 |
22 | @Composable
23 | fun AsyncScreen(
24 | title: String,
25 | onBackClicked: () -> Unit,
26 | content: @Composable (padding: PaddingValues, run: (String, suspend () -> Unit) -> Unit) -> Unit
27 | ) {
28 | val scaffoldState = rememberScaffoldState()
29 | val openDialog = remember { mutableStateOf(false) }
30 | val coroutineScope = rememberCoroutineScope()
31 | val backgroundText = remember {
32 | mutableStateOf("")
33 | }
34 |
35 | val setAsyncTaskLabel: (String) -> Unit = {
36 | backgroundText.value = it
37 | }
38 |
39 | if (openDialog.value) {
40 | BackgroundTask(
41 | text = backgroundText.value
42 | )
43 | } else {
44 | Scaffold(
45 | scaffoldState = scaffoldState,
46 | topBar = {
47 | TopAppBar(title = {
48 | Text(text = title)
49 | },
50 | navigationIcon = {
51 | IconButton(onClick = onBackClicked) {
52 | Icon(
53 | imageVector = Icons.AutoMirrored.Filled.ArrowBack,
54 | contentDescription = "Back"
55 | )
56 | }
57 | })
58 | }
59 | ) { padding ->
60 | content(padding) { label, run ->
61 | coroutineScope.launch {
62 | setAsyncTaskLabel(label)
63 | openDialog.value = true
64 | try {
65 | run()
66 | } catch (e: Exception) {
67 | openDialog.value = false
68 | Log.e(asyncScreenLogTag, e.toString())
69 | coroutineScope.launch {
70 | scaffoldState.snackbarHostState.showSnackbar(
71 | message = e.message.orEmpty(),
72 | duration = SnackbarDuration.Short,
73 | )
74 | }
75 | } catch (err: Exception) {
76 | openDialog.value = false
77 | Log.e(asyncScreenLogTag, err.toString())
78 | coroutineScope.launch {
79 | scaffoldState.snackbarHostState.showSnackbar(
80 | message = "Unknown Error",
81 | duration = SnackbarDuration.Short,
82 | )
83 | }
84 | } finally {
85 | openDialog.value = false
86 | }
87 | }
88 | }
89 | }
90 | }
91 | }
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/components/BackgroundTask.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.components
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material.CircularProgressIndicator
5 | import androidx.compose.material.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Alignment
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.res.stringResource
10 | import androidx.compose.ui.unit.dp
11 | import dev.lbeernaert.youhavemail.R
12 |
13 | @Composable
14 | fun BackgroundTask(text: String) {
15 | Column(
16 | modifier = Modifier
17 | .padding(10.dp)
18 | .fillMaxSize(),
19 | verticalArrangement = Arrangement.Center,
20 | horizontalAlignment = Alignment.CenterHorizontally,
21 |
22 | ) {
23 | CircularProgressIndicator()
24 | Spacer(modifier = Modifier.padding(10.dp))
25 | Text(text = text)
26 | }
27 | }
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/components/PasswordField.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.components
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.foundation.text.KeyboardActions
5 | import androidx.compose.foundation.text.KeyboardOptions
6 | import androidx.compose.material.Icon
7 | import androidx.compose.material.IconButton
8 | import androidx.compose.material.Text
9 | import androidx.compose.material.TextField
10 | import androidx.compose.material.icons.Icons
11 | import androidx.compose.material.icons.filled.Visibility
12 | import androidx.compose.material.icons.filled.VisibilityOff
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.MutableState
15 | import androidx.compose.runtime.mutableStateOf
16 | import androidx.compose.runtime.remember
17 | import androidx.compose.ui.ExperimentalComposeUiApi
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.autofill.AutofillType
20 | import androidx.compose.ui.text.input.ImeAction
21 | import androidx.compose.ui.text.input.KeyboardType
22 | import androidx.compose.ui.text.input.PasswordVisualTransformation
23 | import androidx.compose.ui.text.input.TextFieldValue
24 | import androidx.compose.ui.text.input.VisualTransformation
25 | import dev.lbeernaert.youhavemail.ui.AutoFillRequestHandler
26 | import dev.lbeernaert.youhavemail.ui.autofill
27 |
28 |
29 | @OptIn(ExperimentalComposeUiApi::class)
30 | @Composable
31 | fun PasswordField(
32 | placeHolder: String,
33 | state: MutableState,
34 | onClick: () -> Unit
35 | ) {
36 | val showPassword = remember {
37 | mutableStateOf(false)
38 | }
39 | val autoFillHandler = AutoFillRequestHandler(
40 | autofillTypes = listOf(AutofillType.Password),
41 | onFill = { state.value = TextFieldValue(it) }
42 | )
43 |
44 | TextField(
45 | modifier = Modifier
46 | .fillMaxWidth()
47 | .autofill(handler = autoFillHandler),
48 | label = { Text(text = placeHolder) },
49 | value = state.value,
50 | singleLine = true,
51 | visualTransformation = if (!showPassword.value) {
52 | PasswordVisualTransformation()
53 | } else {
54 | VisualTransformation.None
55 | },
56 | keyboardOptions = KeyboardOptions(
57 | keyboardType = KeyboardType.Password,
58 | imeAction = ImeAction.Done
59 | ),
60 | onValueChange = { state.value = it },
61 | keyboardActions = KeyboardActions(onDone = {
62 | onClick()
63 | }),
64 | trailingIcon = {
65 | val icon = if (showPassword.value) {
66 | Icons.Filled.Visibility
67 | } else {
68 | Icons.Filled.VisibilityOff
69 | }
70 |
71 | IconButton(onClick = { showPassword.value = !showPassword.value }) {
72 | Icon(
73 | icon,
74 | contentDescription = "Visibility",
75 | )
76 | }
77 | }
78 | )
79 | }
80 |
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/screens/AccountInfo.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.screens
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.rememberScrollState
11 | import androidx.compose.foundation.verticalScroll
12 | import androidx.compose.material.Divider
13 | import androidx.compose.material.MaterialTheme
14 | import androidx.compose.material.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.res.stringResource
19 | import androidx.compose.ui.text.font.FontWeight
20 | import androidx.compose.ui.unit.dp
21 | import dev.lbeernaert.youhavemail.Account
22 | import dev.lbeernaert.youhavemail.R
23 | import dev.lbeernaert.youhavemail.app.accountStatusString
24 | import dev.lbeernaert.youhavemail.components.ActionButton
25 | import dev.lbeernaert.youhavemail.components.AsyncScreen
26 |
27 |
28 | @Composable
29 | fun AccountInfo(
30 | account: Account,
31 | onBackClicked: () -> Unit,
32 | onLogout: suspend () -> Unit,
33 | onLogin: () -> Unit,
34 | onDelete: suspend () -> Unit,
35 | onProxyClicked: () -> Unit,
36 | ) {
37 |
38 | AsyncScreen(
39 | title = stringResource(id = R.string.account_title),
40 | onBackClicked = onBackClicked
41 | ) { padding, runTask ->
42 |
43 | val logOutBackgroundLabel = stringResource(id = R.string.logging_out)
44 | val onLogoutImpl: () -> Unit = {
45 | runTask(logOutBackgroundLabel) {
46 | onLogout()
47 | }
48 | }
49 |
50 | val onDeleteImpl: () -> Unit = {
51 | runTask(logOutBackgroundLabel) {
52 | onDelete()
53 | }
54 | }
55 |
56 | Column(
57 | modifier = Modifier
58 | .padding(padding)
59 | .padding(20.dp)
60 | .fillMaxSize()
61 | .verticalScroll(rememberScrollState(), true),
62 | verticalArrangement = Arrangement.Top,
63 | horizontalAlignment = Alignment.CenterHorizontally,
64 | ) {
65 | Text(
66 | text = "Email",
67 | modifier = Modifier.fillMaxWidth(),
68 | fontWeight = FontWeight.Bold,
69 | style = MaterialTheme.typography.h5,
70 | )
71 |
72 | Text(
73 | text = account.email(),
74 | modifier = Modifier.fillMaxWidth(),
75 | )
76 |
77 | Spacer(modifier = Modifier.height(20.dp))
78 |
79 | Text(
80 | text = "Backend",
81 | modifier = Modifier.fillMaxWidth(),
82 | fontWeight = FontWeight.Bold,
83 | style = MaterialTheme.typography.h5,
84 | )
85 |
86 | Text(
87 | text = account.backend(),
88 | modifier = Modifier.fillMaxWidth()
89 | )
90 |
91 | Spacer(modifier = Modifier.height(20.dp))
92 |
93 | Text(
94 | text = stringResource(id = R.string.status_no_colon),
95 | fontWeight = FontWeight.Bold,
96 | modifier = Modifier.fillMaxWidth(),
97 | style = MaterialTheme.typography.h5,
98 | )
99 |
100 | Text(
101 | text = accountStatusString(account),
102 | modifier = Modifier.fillMaxWidth()
103 | )
104 |
105 | Spacer(modifier = Modifier.height(20.dp))
106 |
107 | Divider()
108 |
109 | Spacer(modifier = Modifier.height(20.dp))
110 |
111 | ActionButton(text = stringResource(id = R.string.proxy_settings), onProxyClicked)
112 |
113 | Spacer(modifier = Modifier.height(40.dp))
114 |
115 | if (account.isLoggedOut()) {
116 | ActionButton(
117 | text = stringResource(id = R.string.login),
118 | onClick = onLogin
119 | )
120 | } else {
121 | ActionButton(
122 | text = stringResource(id = R.string.logout),
123 | onClick = onLogoutImpl,
124 | )
125 | }
126 |
127 | Spacer(modifier = Modifier.height(20.dp))
128 |
129 | ActionButton(text = stringResource(id = R.string.delete_account), onDeleteImpl)
130 |
131 | Spacer(modifier = Modifier.height(40.dp))
132 | }
133 | }
134 | }
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/screens/BackendSelection.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.screens
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.foundation.lazy.LazyColumn
7 | import androidx.compose.foundation.lazy.itemsIndexed
8 | import androidx.compose.material.*
9 | import androidx.compose.material.icons.Icons
10 | import androidx.compose.material.icons.automirrored.filled.ArrowBack
11 | import androidx.compose.material.icons.filled.ArrowBack
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.res.stringResource
15 | import androidx.compose.ui.text.font.FontWeight
16 | import androidx.compose.ui.text.style.TextAlign
17 | import androidx.compose.ui.unit.dp
18 | import androidx.compose.ui.unit.sp
19 | import androidx.navigation.NavController
20 | import dev.lbeernaert.youhavemail.Backend
21 | import dev.lbeernaert.youhavemail.R
22 | import dev.lbeernaert.youhavemail.app.State
23 |
24 | @Composable
25 | fun BackendSelection(state: State, navController: NavController) {
26 | Scaffold(topBar = {
27 | TopAppBar(title = {
28 | Text(text = stringResource(id = R.string.backend_title))
29 | },
30 | navigationIcon =
31 | {
32 | IconButton(onClick = {
33 | navController.popBackStack()
34 | }) {
35 | Icon(
36 | imageVector = Icons.AutoMirrored.Filled.ArrowBack,
37 | contentDescription = "Back"
38 | )
39 | }
40 | }
41 | )
42 | },
43 | content = { _ ->
44 | BackendList(backends = state.backends(), onClicked = {
45 | navController.navigate(Routes.newProxyLoginRoute(it))
46 | })
47 | }
48 | )
49 | }
50 |
51 | @Composable
52 | fun BackendListItem(backend: Backend, onClicked: (String) -> Unit) {
53 | Row(
54 | modifier = Modifier
55 | .padding(10.dp)
56 | .fillMaxWidth()
57 | .clickable { onClicked(backend.name()) },
58 | ) {
59 | val name = backend.name()
60 | Column(
61 | verticalArrangement = Arrangement.Center,
62 | modifier = Modifier
63 | .size(60.dp)
64 | .background(MaterialTheme.colors.primary, MaterialTheme.shapes.large),
65 | ) {
66 | Text(
67 | modifier = Modifier.fillMaxWidth(),
68 | text = name.first().toString(),
69 | textAlign = TextAlign.Center,
70 | style = MaterialTheme.typography.button,
71 | fontWeight = FontWeight.Bold,
72 | fontSize = 30.sp
73 | )
74 | }
75 | Spacer(modifier = Modifier.width(10.dp))
76 | Column(modifier = Modifier.fillMaxWidth()) {
77 | Text(
78 | text = name,
79 | style = MaterialTheme.typography.subtitle1,
80 | fontWeight = FontWeight.Bold
81 | )
82 | Text(text = backend.description(), style = MaterialTheme.typography.body2)
83 | }
84 | }
85 | }
86 |
87 | @Composable
88 | fun BackendList(backends: List, onClicked: (index: String) -> Unit) {
89 | LazyColumn(contentPadding = PaddingValues(horizontal = 10.dp, vertical = 10.dp)) {
90 | itemsIndexed(backends) { _, backend ->
91 | BackendListItem(backend = backend, onClicked = onClicked)
92 | }
93 | }
94 | }
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/screens/Login.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.screens
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.text.KeyboardOptions
11 | import androidx.compose.material.Text
12 | import androidx.compose.material.TextField
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.mutableStateOf
15 | import androidx.compose.runtime.remember
16 | import androidx.compose.runtime.saveable.rememberSaveable
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.ExperimentalComposeUiApi
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.autofill.AutofillType
21 | import androidx.compose.ui.res.stringResource
22 | import androidx.compose.ui.text.input.ImeAction
23 | import androidx.compose.ui.text.input.KeyboardType
24 | import androidx.compose.ui.text.input.TextFieldValue
25 | import androidx.compose.ui.unit.dp
26 | import dev.lbeernaert.youhavemail.R
27 | import dev.lbeernaert.youhavemail.components.ActionButton
28 | import dev.lbeernaert.youhavemail.components.AsyncScreen
29 | import dev.lbeernaert.youhavemail.components.PasswordField
30 | import dev.lbeernaert.youhavemail.ui.AutoFillRequestHandler
31 | import dev.lbeernaert.youhavemail.ui.autofill
32 |
33 |
34 | @OptIn(ExperimentalComposeUiApi::class)
35 | @Composable
36 | fun Login(
37 | backendName: String,
38 | accountEmail: String,
39 | onBackClicked: () -> Unit,
40 | onLoginClicked: suspend (email: String, password: String) -> Unit
41 | ) {
42 | val email = rememberSaveable(stateSaver = TextFieldValue.Saver) {
43 | mutableStateOf(
44 | TextFieldValue(accountEmail)
45 | )
46 | }
47 | val password =
48 | remember { mutableStateOf(TextFieldValue()) }
49 | val loginBackgroundLabel = stringResource(id = R.string.login_to_account, email.value.text)
50 |
51 | AsyncScreen(
52 | title = stringResource(id = R.string.login),
53 | onBackClicked = onBackClicked
54 | ) { padding, runTask ->
55 |
56 | val onClick: () -> Unit = {
57 | runTask(loginBackgroundLabel) {
58 | onLoginClicked(email.value.text, password.value.text)
59 | }
60 | }
61 | val autoFillHandler = AutoFillRequestHandler(
62 | autofillTypes = listOf(AutofillType.EmailAddress),
63 | onFill = { email.value = TextFieldValue(it) }
64 | )
65 |
66 | Column(
67 | modifier = Modifier
68 | .padding(padding)
69 | .padding(20.dp)
70 | .fillMaxSize(),
71 | verticalArrangement = Arrangement.Center,
72 | horizontalAlignment = Alignment.CenterHorizontally,
73 |
74 | ) {
75 | Text(text = stringResource(R.string.login_to_account, backendName))
76 |
77 | Spacer(modifier = Modifier.height(20.dp))
78 |
79 | TextField(
80 | modifier = Modifier.fillMaxWidth()
81 | .autofill(handler = autoFillHandler),
82 | label = { Text(text = "Email") },
83 | singleLine = true,
84 | value = email.value,
85 | onValueChange = { email.value = it },
86 | keyboardOptions = KeyboardOptions(
87 | keyboardType = KeyboardType.Email,
88 | imeAction = ImeAction.Next
89 | ),
90 | )
91 |
92 | Spacer(modifier = Modifier.height(20.dp))
93 |
94 | PasswordField(placeHolder = "Password", state = password, onClick = onClick)
95 |
96 | Spacer(modifier = Modifier.height(20.dp))
97 |
98 | ActionButton(text = stringResource(id = R.string.login), onClick)
99 | }
100 |
101 | }
102 | }
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/screens/Main.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.screens
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.PaddingValues
8 | import androidx.compose.foundation.layout.Row
9 | import androidx.compose.foundation.layout.Spacer
10 | import androidx.compose.foundation.layout.fillMaxSize
11 | import androidx.compose.foundation.layout.fillMaxWidth
12 | import androidx.compose.foundation.layout.padding
13 | import androidx.compose.foundation.layout.size
14 | import androidx.compose.foundation.layout.width
15 | import androidx.compose.foundation.lazy.LazyColumn
16 | import androidx.compose.foundation.lazy.itemsIndexed
17 | import androidx.compose.material.Button
18 | import androidx.compose.material.ButtonDefaults
19 | import androidx.compose.material.FabPosition
20 | import androidx.compose.material.FloatingActionButton
21 | import androidx.compose.material.Icon
22 | import androidx.compose.material.MaterialTheme
23 | import androidx.compose.material.Scaffold
24 | import androidx.compose.material.Text
25 | import androidx.compose.material.TopAppBar
26 | import androidx.compose.material.icons.Icons
27 | import androidx.compose.material.icons.filled.Add
28 | import androidx.compose.material.icons.filled.Refresh
29 | import androidx.compose.material.icons.filled.Settings
30 | import androidx.compose.runtime.Composable
31 | import androidx.compose.runtime.collectAsState
32 | import androidx.compose.runtime.getValue
33 | import androidx.compose.runtime.mutableStateOf
34 | import androidx.compose.runtime.remember
35 | import androidx.compose.runtime.setValue
36 | import androidx.compose.ui.Alignment
37 | import androidx.compose.ui.Modifier
38 | import androidx.compose.ui.res.stringResource
39 | import androidx.compose.ui.text.font.FontWeight
40 | import androidx.compose.ui.text.style.TextAlign
41 | import androidx.compose.ui.unit.dp
42 | import androidx.compose.ui.unit.sp
43 | import androidx.navigation.NavController
44 | import dev.lbeernaert.youhavemail.Account
45 | import dev.lbeernaert.youhavemail.R
46 | import dev.lbeernaert.youhavemail.app.State
47 | import dev.lbeernaert.youhavemail.app.accountStatusString
48 | import java.util.Timer
49 | import java.util.TimerTask
50 |
51 | @Composable
52 | fun Main(
53 | state: State,
54 | navController: NavController,
55 | requestPermissions: () -> Unit,
56 | onSettingsClicked: () -> Unit,
57 | onPollClicked: () -> Unit,
58 | ) {
59 | AccountList(
60 | state = state,
61 | navController = navController,
62 | requestPermissions = requestPermissions,
63 | onSettingsClicked = onSettingsClicked,
64 | onPollClicked = onPollClicked,
65 | )
66 | }
67 |
68 |
69 | @Composable
70 | fun AccountList(
71 | state: State,
72 | navController: NavController,
73 | requestPermissions: () -> Unit,
74 | onSettingsClicked: () -> Unit,
75 | onPollClicked: () -> Unit,
76 | ) {
77 | val accounts by state.accounts().collectAsState()
78 | var pollActive by remember { mutableStateOf(true) }
79 |
80 | Scaffold(topBar = {
81 | TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }, actions = {
82 | Button(
83 | onClick = {
84 | onPollClicked()
85 | pollActive = false
86 | Timer().schedule(
87 | object : TimerTask() {
88 | override fun run() {
89 | pollActive = true
90 | }
91 | }, 15000
92 | )
93 | },
94 | colors = ButtonDefaults.outlinedButtonColors(),
95 | enabled = pollActive,
96 | ) {
97 | Icon(
98 | Icons.Filled.Refresh,
99 | contentDescription = "Poll accounts",
100 | modifier = Modifier.size(30.dp)
101 | )
102 | }
103 | Spacer(modifier = Modifier.size(10.dp))
104 | Button(
105 | onClick = onSettingsClicked,
106 | colors = ButtonDefaults.outlinedButtonColors()
107 | ) {
108 | Icon(
109 | Icons.Filled.Settings,
110 | contentDescription = "Settings",
111 | modifier = Modifier.size(30.dp)
112 | )
113 | }
114 | })
115 | },
116 | floatingActionButtonPosition = FabPosition.End,
117 | floatingActionButton = {
118 | FloatingActionButton(onClick = {
119 | requestPermissions()
120 | navController.navigate(Routes.Backend.route)
121 | }) {
122 | Icon(Icons.Filled.Add, "")
123 | }
124 | },
125 | content = { padding ->
126 | val isEmpty = accounts.isEmpty()
127 | Column(
128 | modifier = Modifier
129 | .padding(padding)
130 | .fillMaxSize(),
131 | verticalArrangement = if (isEmpty) {
132 | Arrangement.Center
133 | } else {
134 | Arrangement.Top
135 | },
136 | horizontalAlignment = if (isEmpty) {
137 | Alignment.CenterHorizontally
138 | } else {
139 | Alignment.Start
140 | }
141 |
142 | ) {
143 | if (accounts.isEmpty()) {
144 | Text(text = stringResource(id = R.string.no_accounts))
145 | } else {
146 | LazyColumn(
147 | contentPadding = PaddingValues(
148 | horizontal = 10.dp,
149 | vertical = 10.dp
150 | )
151 | ) {
152 | itemsIndexed(accounts) { _, account ->
153 | ActiveAccount(account = account, onClicked = {
154 | navController.navigate(Routes.newAccountRoute(it))
155 | })
156 | }
157 | }
158 | }
159 | }
160 | })
161 | }
162 |
163 |
164 | @Composable
165 | fun ActiveAccount(account: Account, onClicked: (String) -> Unit) {
166 | Row(
167 | modifier = Modifier
168 | .padding(10.dp)
169 | .fillMaxWidth()
170 | .clickable { onClicked(account.email()) },
171 | ) {
172 | val email = account.email()
173 |
174 | Column(
175 | verticalArrangement = Arrangement.Center,
176 | modifier = Modifier
177 | .size(60.dp)
178 | .background(MaterialTheme.colors.primary, MaterialTheme.shapes.large),
179 | ) {
180 | Text(
181 | modifier = Modifier.fillMaxWidth(),
182 | text = email.first().toString().uppercase(),
183 | textAlign = TextAlign.Center,
184 | style = MaterialTheme.typography.button,
185 | fontWeight = FontWeight.Bold,
186 | fontSize = 30.sp
187 | )
188 | }
189 | Spacer(modifier = Modifier.width(10.dp))
190 | Column(modifier = Modifier.fillMaxWidth()) {
191 | Text(
192 | text = email,
193 | style = MaterialTheme.typography.subtitle1,
194 | fontWeight = FontWeight.Bold
195 | )
196 |
197 | Text(
198 | text = stringResource(id = R.string.status, accountStatusString(account)),
199 | style = MaterialTheme.typography.body2
200 | )
201 | }
202 | }
203 | }
204 |
205 |
206 |
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/screens/Nav.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.screens
2 |
3 | import android.app.Activity
4 | import android.util.Log
5 | import android.widget.Toast
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.res.stringResource
8 | import androidx.navigation.NavType
9 | import androidx.navigation.compose.NavHost
10 | import androidx.navigation.compose.composable
11 | import androidx.navigation.compose.rememberNavController
12 | import androidx.navigation.navArgument
13 | import dev.lbeernaert.youhavemail.R
14 | import dev.lbeernaert.youhavemail.app.State
15 | import dev.lbeernaert.youhavemail.testProxy
16 | import kotlinx.coroutines.Dispatchers
17 | import kotlinx.coroutines.withContext
18 |
19 | const val navLogTag = "nav"
20 |
21 | @Composable
22 | fun MainNavController(
23 | context: Activity,
24 | state: State,
25 | requestPermissions: () -> Unit,
26 | onPollClicked: () -> Unit,
27 | onExportLogsClicked: () -> Unit
28 | ) {
29 | val navController = rememberNavController()
30 |
31 | NavHost(navController = navController, startDestination = Routes.Main.route) {
32 | // ------------------ LOGIN --------------------------------------------------------------
33 | composable(
34 | Routes.Login.route,
35 | arguments = listOf(
36 | navArgument("email") {
37 | type = NavType.StringType
38 | defaultValue = ""
39 | })
40 | ) {
41 | val args = it.arguments
42 | val accountEmail = args?.getString("email").orEmpty()
43 | if (state.mLoginSequence == null) {
44 | Log.e(navLogTag, "No backend index selected, returning to main screen")
45 | navController.popBackStack(Routes.Main.route, false)
46 | } else {
47 | Login(
48 | accountEmail = accountEmail,
49 | backendName = state.mLoginSequence!!.backendName(),
50 | onBackClicked = {
51 | navController.popBackStack()
52 | },
53 | onLoginClicked = { email, password ->
54 | if(email.isBlank() or password.isEmpty()) {
55 | Toast.makeText(
56 | context,
57 | R.string.empty_email_or_password_info,
58 | Toast.LENGTH_SHORT
59 | ).show()
60 | } else if(!android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
61 | Toast.makeText(
62 | context,
63 | R.string.invalid_email_address,
64 | Toast.LENGTH_SHORT
65 | ).show()
66 | }
67 | else {
68 | withContext(Dispatchers.Default) {
69 | state.mLoginSequence!!.login(email, password)
70 | }
71 | state.mLoginSequence!!.next(navController)
72 | }
73 | }
74 | )
75 | }
76 | }
77 | // ------------------ TOTP ---------------------------------------------------------------
78 | composable(Routes.TOTP.route) {
79 | val onTotpClicked: suspend (value: String) -> Unit =
80 | { totp ->
81 | withContext(Dispatchers.Default) {
82 | state.mLoginSequence!!.totp(totp)
83 | }
84 | state.mLoginSequence!!.next(navController)
85 | }
86 | Totp(onBackClicked = {
87 | navController.popBackStack()
88 | }, onTotpClicked = onTotpClicked)
89 | }
90 | // ------------------ MAIN ---------------------------------------------------------------
91 | composable(Routes.Main.route) {
92 | Main(state, navController, requestPermissions, onSettingsClicked = {
93 | navController.navigate(Routes.Settings.route)
94 | }, onPollClicked)
95 | }
96 | // ------------------ BACKEND SELECTION --------------------------------------------------
97 | composable(Routes.Backend.route) {
98 | BackendSelection(state = state, navController = navController)
99 | }
100 | // ------------------ ACCOUNT DETAILS ----------------------------------------------------
101 | composable(
102 | Routes.Account.route,
103 | arguments = listOf(navArgument("email") { type = NavType.StringType })
104 | ) {
105 | val accountEmail = it.arguments?.getString("email")
106 | if (accountEmail == null) {
107 | Log.e(navLogTag, "No account index selected, returning to main screen")
108 | navController.popBackStack(Routes.Main.route, false)
109 | }
110 | val account = state.account(accountEmail!!)
111 | if (account == null) {
112 | Log.e(navLogTag, "Account not found, return to main screen")
113 | navController.popBackStack(Routes.Main.route, false)
114 | } else {
115 | AccountInfo(
116 | account = account,
117 | onBackClicked = {
118 | navController.popBackStack(Routes.Main.route, false)
119 | },
120 | onLogout = {
121 | withContext(Dispatchers.Default) {
122 | state.logout(accountEmail)
123 | }
124 | },
125 | onLogin = {
126 | state.mLoginSequence =
127 | state.newLoginSequence(account.backend(), account.proxy())
128 | navController.navigate(Routes.newLoginRoute(accountEmail))
129 | },
130 | onDelete = {
131 | withContext(Dispatchers.Default) {
132 | state.delete(accountEmail)
133 | }
134 | navController.popBackStack(Routes.Main.route, false)
135 | },
136 | onProxyClicked = {
137 | navController.navigate(Routes.newAccountProxyRoute(accountEmail))
138 | }
139 | )
140 | }
141 | }
142 | // ------------------ SETTINGS -----------------------------------------------------------
143 | composable(Routes.Settings.route) {
144 | Settings(
145 | state = state,
146 | onBackClicked = {
147 | navController.popBackStack()
148 | },
149 | onPollIntervalUpdate = { interval ->
150 | state.setPollInterval(context, interval)
151 | },
152 | onExportLogsClicked = onExportLogsClicked
153 | )
154 | }
155 | // ------------------ PROXY LOGIN --------------------------------------------------------
156 | composable(
157 | Routes.ProxyLogin.route,
158 | arguments = listOf(navArgument("name") { type = NavType.StringType })
159 | ) {
160 | val args = it.arguments
161 | val backendName = args?.getString("name")
162 | if (backendName == null) {
163 | Log.e(navLogTag, "No backend name selected, returning to main screen")
164 | navController.popBackStack(Routes.Main.route, false)
165 | } else {
166 | ProxyScreen(
167 | onBackClicked = {
168 | navController.popBackStack()
169 | },
170 | applyButtonText = stringResource(id = R.string.next),
171 | onApplyClicked = { proxy ->
172 | withContext(Dispatchers.Default) {
173 | if (proxy != null) {
174 | testProxy(proxy)
175 | }
176 | }
177 | state.mLoginSequence = state.newLoginSequence(backendName, proxy)
178 | navController.navigate(Routes.newLoginRoute(null))
179 | },
180 | proxy = null,
181 | isLoginRequest = true,
182 | )
183 | }
184 | }
185 | // ------------------ PROXY SETTINGS -----------------------------------------------------
186 | composable(
187 | Routes.ProxySettings.route,
188 | arguments = listOf(navArgument("email") { type = NavType.StringType })
189 | ) {
190 | val accountEmail = it.arguments?.getString("email")
191 | if (accountEmail == null) {
192 | Log.e(navLogTag, "No account index selected, returning to main screen")
193 | navController.popBackStack(Routes.Main.route, false)
194 | }
195 |
196 | val account = state.account(accountEmail!!)
197 | if (account == null) {
198 | Log.e(navLogTag, "Account not found, return to main screen")
199 | navController.popBackStack(Routes.Main.route, false)
200 | } else {
201 | ProxyScreen(
202 | onBackClicked = {
203 | navController.popBackStack()
204 | },
205 | applyButtonText = stringResource(id = R.string.apply),
206 | onApplyClicked = { proxy ->
207 | withContext(Dispatchers.Default) {
208 | account.setProxy(proxy)
209 | }
210 | navController.popBackStack()
211 | },
212 | proxy = account.proxy(),
213 | isLoginRequest = false,
214 | )
215 | }
216 | }
217 | // ------------------ PROTON CAPTCHA -----------------------------------------------------
218 | composable(Routes.ProtonCaptcha.route) {
219 | ProtonCaptchaScreen(
220 | onBackClicked = {
221 | navController.popBackStack()
222 | },
223 | onCaptchaSuccess = {
224 | withContext(Dispatchers.Default) {
225 | state.mLoginSequence!!.protonCaptcha(it)
226 | }
227 | state.mLoginSequence!!.next(navController)
228 | },
229 | onCaptchaFail = {
230 | Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
231 | navController.popBackStack()
232 | },
233 | html = state.mLoginSequence!!.protonCaptchaHtml(),
234 | )
235 | }
236 | }
237 | }
238 |
239 |
240 |
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/screens/ProtonCaptcha.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.screens
2 |
3 | import android.net.http.SslError
4 | import android.util.Log
5 | import android.view.ViewGroup
6 | import android.webkit.ConsoleMessage
7 | import android.webkit.JavascriptInterface
8 | import android.webkit.SslErrorHandler
9 | import android.webkit.WebChromeClient
10 | import android.webkit.WebResourceError
11 | import android.webkit.WebResourceRequest
12 | import android.webkit.WebResourceResponse
13 | import android.webkit.WebView
14 | import android.webkit.WebViewClient
15 | import androidx.compose.foundation.layout.Column
16 | import androidx.compose.foundation.layout.fillMaxSize
17 | import androidx.compose.foundation.layout.padding
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.res.stringResource
21 | import androidx.compose.ui.unit.dp
22 | import androidx.compose.ui.viewinterop.AndroidView
23 | import dev.lbeernaert.youhavemail.R
24 | import dev.lbeernaert.youhavemail.components.AsyncScreen
25 | import java.io.InputStream
26 | import java.text.SimpleDateFormat
27 | import java.util.Date
28 | import java.util.Locale
29 |
30 |
31 | const val PMCaptchaLogTag = "pm-captcha"
32 |
33 | @Composable
34 | fun ProtonCaptchaScreen(
35 | onBackClicked: () -> Unit,
36 | onCaptchaSuccess: suspend (String) -> Unit,
37 | onCaptchaFail: (String) -> Unit,
38 | html: String,
39 | ) {
40 | AsyncScreen(
41 | title = stringResource(id = R.string.captcha_request),
42 | onBackClicked = onBackClicked
43 | ) { _, runTask ->
44 | Column(
45 | modifier = Modifier
46 | .padding(0.dp)
47 | .fillMaxSize()
48 | ) {
49 | val taskLabel = stringResource(id = R.string.retry_login_with_captcha)
50 | AndroidView(factory = {
51 | WebView.setWebContentsDebuggingEnabled(true)
52 | WebView(it).apply {
53 | layoutParams = ViewGroup.LayoutParams(
54 | ViewGroup.LayoutParams.MATCH_PARENT,
55 | ViewGroup.LayoutParams.MATCH_PARENT
56 | )
57 | webViewClient = CaptchaWebView(
58 | onCaptchaFail = onCaptchaFail,
59 | html = html,
60 | )
61 | webChromeClient = object : WebChromeClient() {
62 | override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
63 | Log.d(PMCaptchaLogTag, "${consoleMessage!!.message()}")
64 | return super.onConsoleMessage(consoleMessage)
65 | }
66 | }
67 | addJavascriptInterface(object {
68 | @JavascriptInterface
69 | fun receiveResponse(data: String) {
70 | Log.d(PMCaptchaLogTag, "Captcha solved")
71 | // JSON expected by the backend.
72 | val captchaResponse =
73 | "{\"hv_type\":\"Captcha\", \"hv_token\":\"${data}\"}"
74 | runTask(taskLabel) {
75 | onCaptchaSuccess(captchaResponse)
76 | }
77 | }
78 |
79 | @JavascriptInterface
80 | fun receiveExpiredResponse(data: String) {
81 | Log.d(PMCaptchaLogTag, "Received expired response:$data")
82 | onCaptchaFail("Captcha expired")
83 | }
84 | }, "AndroidInterface")
85 | settings.javaScriptEnabled = true
86 | settings.allowContentAccess = false
87 | settings.allowFileAccess = false
88 | loadUrl("https://mail.proton.me/core/v4/captcha")
89 | }
90 | }, update = {
91 | })
92 | }
93 | }
94 | }
95 |
96 |
97 | class CaptchaWebView(var html: String, var onCaptchaFail: (String) -> Unit) :
98 | WebViewClient() {
99 |
100 | private val formatter: SimpleDateFormat = SimpleDateFormat("E, dd MMM yyyy kk:mm:ss", Locale.US)
101 |
102 | override fun onReceivedError(
103 | view: WebView?,
104 | request: WebResourceRequest?,
105 | error: WebResourceError?
106 | ) {
107 | super.onReceivedError(view, request, error)
108 | Log.e(PMCaptchaLogTag, "WebView Received Error: $error")
109 | onCaptchaFail("WebView Received Error: $error")
110 | }
111 |
112 | override fun onReceivedHttpError(
113 | view: WebView?,
114 | request: WebResourceRequest?,
115 | errorResponse: WebResourceResponse?
116 | ) {
117 | super.onReceivedHttpError(view, request, errorResponse)
118 | Log.e(PMCaptchaLogTag, "WebView Received HTTP Error: $errorResponse")
119 | onCaptchaFail("WebView Received HTTP Error: $errorResponse")
120 | }
121 |
122 | override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
123 | super.onReceivedSslError(view, handler, error)
124 | Log.e(PMCaptchaLogTag, "WebView Received SSL Error: $error")
125 | onCaptchaFail("WebView Received SSL Error: $error")
126 | }
127 |
128 | override fun shouldInterceptRequest(
129 | view: WebView?,
130 | request: WebResourceRequest
131 | ): WebResourceResponse? {
132 | if (request.url.schemeSpecificPart.startsWith("//mail.proton.me")) {
133 | Log.d(PMCaptchaLogTag, "Intercepting request")
134 | var headers = HashMap()
135 | headers.put(
136 | "Access-Control-Allow-Origin", "*"
137 | )
138 | headers.put("Connection", "close")
139 | headers.put("Date", "${formatter.format(Date())} GMT")
140 | headers.put("Content-Type", "text/html")
141 | headers.put("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS")
142 | headers.put("Access-Control-Max-Age", "600")
143 | headers.put("Access-Control-Allow-Credentials", "true")
144 | headers.put("Access-Control-Allow-Headers", "accept, authorization, Content-Type")
145 | headers.put("Via", "you-have-mail")
146 |
147 | var reader: InputStream = html.byteInputStream()
148 | return WebResourceResponse(
149 | "text/html",
150 | "UTF-8",
151 | 200,
152 | "OK",
153 | headers,
154 | reader,
155 | )
156 | }
157 | return super.shouldInterceptRequest(view, request)
158 | }
159 | }
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/screens/Routes.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.screens
2 |
3 | sealed class Routes(val route: String) {
4 | object Account : Routes("Account/{email}")
5 | object Login : Routes("Login?email={email}")
6 | object TOTP : Routes("TOTP")
7 | object Main : Routes("Main")
8 | object Backend : Routes("Backend")
9 |
10 | object Settings : Routes("Settings")
11 | object ProxyLogin : Routes("ProxyLogin/{name}")
12 | object ProxySettings : Routes("ProxySettings/{email}")
13 | object ProtonCaptcha : Routes("ProtonCaptcha")
14 |
15 | companion object {
16 | fun newLoginRoute(email: String?): String {
17 | if (email != null) {
18 | return "Login?email=$email"
19 | }
20 |
21 | return "Login"
22 | }
23 |
24 | fun newProxyLoginRoute(backendEmail: String): String {
25 | return "ProxyLogin/$backendEmail"
26 | }
27 |
28 | fun newAccountRoute(accountEmail: String): String {
29 | return "Account/$accountEmail"
30 | }
31 |
32 | fun newAccountProxyRoute(accountEmail: String): String {
33 | return "ProxySettings/$accountEmail"
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/screens/Settings.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.screens
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.layout.width
10 | import androidx.compose.material.Divider
11 | import androidx.compose.material.DropdownMenu
12 | import androidx.compose.material.DropdownMenuItem
13 | import androidx.compose.material.ExperimentalMaterialApi
14 | import androidx.compose.material.ExposedDropdownMenuBox
15 | import androidx.compose.material.ExposedDropdownMenuDefaults
16 | import androidx.compose.material.MaterialTheme
17 | import androidx.compose.material.Text
18 | import androidx.compose.material.TextField
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.runtime.collectAsState
21 | import androidx.compose.runtime.getValue
22 | import androidx.compose.runtime.mutableIntStateOf
23 | import androidx.compose.runtime.mutableStateOf
24 | import androidx.compose.runtime.remember
25 | import androidx.compose.runtime.setValue
26 | import androidx.compose.ui.Modifier
27 | import androidx.compose.ui.geometry.Size
28 | import androidx.compose.ui.layout.onGloballyPositioned
29 | import androidx.compose.ui.platform.LocalDensity
30 | import androidx.compose.ui.res.stringResource
31 | import androidx.compose.ui.text.style.TextAlign
32 | import androidx.compose.ui.unit.dp
33 | import androidx.compose.ui.unit.toSize
34 | import dev.lbeernaert.youhavemail.R
35 | import dev.lbeernaert.youhavemail.app.State
36 | import dev.lbeernaert.youhavemail.components.ActionButton
37 | import dev.lbeernaert.youhavemail.components.AsyncScreen
38 |
39 |
40 | @OptIn(ExperimentalMaterialApi::class)
41 | @Composable
42 | fun Settings(
43 | state: State,
44 | onBackClicked: () -> Unit,
45 | onPollIntervalUpdate: (ULong) -> Unit,
46 | onExportLogsClicked: () -> Unit,
47 | ) {
48 | AsyncScreen(
49 | title = stringResource(id = R.string.settings),
50 | onBackClicked = onBackClicked
51 | ) { _, runTask ->
52 |
53 | val pollIntervalValue by state.getPollInterval().collectAsState()
54 | val updatingPollIntervalLabel = stringResource(id = R.string.update_poll_interval)
55 |
56 | Column(
57 | modifier = Modifier
58 | .padding(20.dp)
59 | .fillMaxSize()
60 | ) {
61 | val timeIntervals =
62 | listOf(30UL, 60UL, 120UL, 180UL, 300UL, 600UL, 900UL, 1200UL, 1800UL, 3600UL)
63 | var expanded by remember { mutableStateOf(false) }
64 | var selectedIndex by remember { mutableIntStateOf(0) }
65 | // textFieldWidth is used to assign to DropDownMenu the same width as TextField
66 | var textFieldWidth by remember { mutableStateOf(Size.Zero)}
67 |
68 | val onPollIntervalModified: () -> Unit = {
69 | runTask(updatingPollIntervalLabel) {
70 | onPollIntervalUpdate(timeIntervals[selectedIndex])
71 | }
72 | }
73 |
74 | Text(
75 | text = stringResource(id = R.string.poll_interval),
76 | textAlign = TextAlign.Center,
77 | style = MaterialTheme.typography.h6,
78 | modifier = Modifier.fillMaxWidth()
79 | )
80 | Spacer(modifier = Modifier.padding(5.dp))
81 | Text(
82 | text = stringResource(id = R.string.poll_interval_desc),
83 | style = MaterialTheme.typography.subtitle1
84 | )
85 |
86 | Spacer(modifier = Modifier.padding(5.dp))
87 |
88 | ExposedDropdownMenuBox(
89 | modifier = Modifier
90 | .fillMaxWidth(),
91 | expanded = expanded,
92 | onExpandedChange = {
93 | expanded = !expanded
94 | }
95 | ) {
96 | TextField(
97 | modifier = Modifier
98 | .fillMaxWidth()
99 | .clickable { expanded = !expanded }
100 | .onGloballyPositioned { coordinates ->
101 | textFieldWidth = coordinates.size.toSize()
102 | },
103 | readOnly = true,
104 | enabled = true,
105 | value = secondsToText(pollIntervalValue),
106 | onValueChange = { },
107 | trailingIcon = {
108 | ExposedDropdownMenuDefaults.TrailingIcon(
109 | expanded = expanded,
110 | onIconClick = { expanded = true }
111 | )
112 | },
113 | colors = ExposedDropdownMenuDefaults.textFieldColors()
114 | )
115 | DropdownMenu(
116 | modifier = Modifier
117 | .width(with(LocalDensity.current) { textFieldWidth.width.toDp() }),
118 | expanded = expanded,
119 | onDismissRequest = {
120 | expanded = false
121 | },
122 | ) {
123 | timeIntervals.forEachIndexed { index, seconds ->
124 | DropdownMenuItem(
125 | modifier = Modifier
126 | .fillMaxWidth(),
127 | onClick = {
128 | expanded = false
129 | selectedIndex = index
130 | onPollIntervalModified()
131 | },
132 | ) {
133 | Text(text = secondsToText(seconds))
134 | }
135 | }
136 | }
137 | }
138 |
139 | Spacer(modifier = Modifier.padding(5.dp))
140 |
141 | Divider()
142 |
143 | Spacer(modifier = Modifier.padding(5.dp))
144 |
145 | ActionButton(
146 | text = stringResource(id = R.string.export_logs),
147 | onClick = onExportLogsClicked
148 | )
149 |
150 | Spacer(modifier = Modifier.padding(5.dp))
151 | }
152 | }
153 | }
154 |
155 | @Composable
156 | fun secondsToText(seconds: ULong): String {
157 | val secondStr = stringResource(id = R.string.second)
158 | val secondsStr = stringResource(id = R.string.seconds)
159 | val minuteStr = stringResource(id = R.string.minute)
160 | val minutesStr = stringResource(id = R.string.minutes)
161 | return if(seconds == 1UL) {
162 | "$seconds $secondStr"
163 | } else if (seconds < 60UL) {
164 | "$seconds $secondsStr"
165 | } else if((seconds == 60UL) ) {
166 | "${(seconds / 60UL)} $minuteStr"
167 | } else {
168 | "${(seconds / 60UL)} $minutesStr"
169 | }
170 | }
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/screens/Totp.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.screens
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.foundation.text.KeyboardActions
5 | import androidx.compose.foundation.text.KeyboardOptions
6 | import androidx.compose.material.Text
7 | import androidx.compose.material.TextField
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.mutableStateOf
10 | import androidx.compose.runtime.remember
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.res.stringResource
14 | import androidx.compose.ui.text.input.ImeAction
15 | import androidx.compose.ui.text.input.KeyboardType
16 | import androidx.compose.ui.text.input.PasswordVisualTransformation
17 | import androidx.compose.ui.text.input.TextFieldValue
18 | import androidx.compose.ui.unit.dp
19 | import dev.lbeernaert.youhavemail.R
20 | import dev.lbeernaert.youhavemail.components.ActionButton
21 | import dev.lbeernaert.youhavemail.components.AsyncScreen
22 |
23 | @Composable
24 | fun Totp(
25 | onBackClicked: () -> Unit,
26 | onTotpClicked: suspend (value: String) -> Unit
27 | ) {
28 | val totp = remember { mutableStateOf(TextFieldValue()) }
29 | val totpBackgroundLabel = stringResource(id = R.string.submitting_totp)
30 |
31 | AsyncScreen(
32 | title = stringResource(id = R.string.totp_title),
33 | onBackClicked = onBackClicked
34 | ) { padding, runTask ->
35 | val onClick: () -> Unit = {
36 | runTask(totpBackgroundLabel) {
37 | onTotpClicked(totp.value.text)
38 | }
39 | }
40 |
41 | Column(
42 | modifier = Modifier
43 | .padding(padding)
44 | .padding(20.dp)
45 | .fillMaxSize(),
46 | verticalArrangement = Arrangement.Center,
47 | horizontalAlignment = Alignment.CenterHorizontally,
48 |
49 | ) {
50 | Text(text = stringResource(R.string.totp_request))
51 |
52 | Spacer(modifier = Modifier.height(20.dp))
53 |
54 | TextField(
55 | modifier = Modifier.fillMaxWidth(),
56 | label = { Text(text = "TOTP") },
57 | value = totp.value,
58 | singleLine = true,
59 | visualTransformation = PasswordVisualTransformation(),
60 | keyboardOptions = KeyboardOptions(
61 | keyboardType = KeyboardType.Number,
62 | imeAction = ImeAction.Done
63 | ),
64 | onValueChange = { totp.value = it },
65 | keyboardActions = KeyboardActions(onDone = {
66 | onClick()
67 | })
68 | )
69 |
70 | Spacer(modifier = Modifier.height(20.dp))
71 |
72 | ActionButton(text = stringResource(id = R.string.submit), onClick = onClick)
73 | }
74 |
75 |
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/ui/Autofill.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.ui
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.remember
5 | import androidx.compose.ui.ExperimentalComposeUiApi
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.autofill.AutofillNode
8 | import androidx.compose.ui.autofill.AutofillType
9 | import androidx.compose.ui.focus.onFocusChanged
10 | import androidx.compose.ui.layout.boundsInWindow
11 | import androidx.compose.ui.layout.onGloballyPositioned
12 | import androidx.compose.ui.platform.LocalAutofill
13 | import androidx.compose.ui.platform.LocalAutofillTree
14 |
15 | // This file is adapted from https://medium.com/@bagadeshrp/compose-ui-textfield-autofill-6e2ac434e380
16 |
17 | // The autofill modifier internally adds two modifiers:
18 | // one to setup the layout, and one to listen for focus events.
19 | @OptIn(ExperimentalComposeUiApi::class)
20 | fun Modifier.autofill(handler: AutoFillHandler): Modifier {
21 | return this.then(
22 | onGloballyPositioned {
23 | handler.autoFillNode.boundingBox = it.boundsInWindow()
24 | }
25 | ).then(
26 | onFocusChanged {
27 | if (it.isFocused) {
28 | handler.request()
29 | } else {
30 | handler.cancel()
31 | }
32 | }
33 | )
34 | }
35 |
36 | @OptIn(ExperimentalComposeUiApi::class)
37 | @Composable
38 | fun AutoFillRequestHandler(
39 | autofillTypes: List = listOf(),
40 | onFill: (String) -> Unit,
41 | ): AutoFillHandler {
42 | val autoFillNode = remember {
43 | AutofillNode(
44 | autofillTypes = autofillTypes,
45 | onFill = { onFill(it) }
46 | )
47 | }
48 | val autofill = LocalAutofill.current
49 | LocalAutofillTree.current += autoFillNode
50 | return remember {
51 | object : AutoFillHandler {
52 | override val autoFillNode: AutofillNode
53 | get() = autoFillNode
54 |
55 | override fun request() {
56 | autofill?.requestAutofillForNode(autofillNode = autoFillNode)
57 | }
58 |
59 | override fun cancel() {
60 | autofill?.cancelAutofillForNode(autofillNode = autoFillNode)
61 | }
62 | }
63 | }
64 | }
65 |
66 | @OptIn(ExperimentalComposeUiApi::class)
67 | interface AutoFillHandler {
68 | val autoFillNode: AutofillNode
69 | fun request()
70 | fun cancel()
71 | }
72 |
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple200 = Color(0xFFBB86FC)
6 | val Purple500 = Color(0xFF6200EE)
7 | val Purple700 = Color(0xFF3700B3)
8 | val Teal200 = Color(0xFF03DAC5)
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes = Shapes(
8 | small = RoundedCornerShape(4.dp),
9 | medium = RoundedCornerShape(4.dp),
10 | large = RoundedCornerShape(0.dp)
11 | )
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.material.darkColors
6 | import androidx.compose.material.lightColors
7 | import androidx.compose.runtime.Composable
8 |
9 | private val DarkColorPalette = darkColors(
10 | primary = Purple200,
11 | primaryVariant = Purple700,
12 | secondary = Teal200
13 | )
14 |
15 | private val LightColorPalette = lightColors(
16 | primary = Purple500,
17 | primaryVariant = Purple700,
18 | secondary = Teal200
19 |
20 | /* Other default colors to override
21 | background = Color.White,
22 | surface = Color.White,
23 | onPrimary = Color.White,
24 | onSecondary = Color.Black,
25 | onBackground = Color.Black,
26 | onSurface = Color.Black,
27 | */
28 | )
29 |
30 | @Composable
31 | fun YouHaveMailTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
32 | val colors = if (darkTheme) {
33 | DarkColorPalette
34 | } else {
35 | LightColorPalette
36 | }
37 |
38 | MaterialTheme(
39 | colors = colors,
40 | typography = Typography,
41 | shapes = Shapes,
42 | content = content
43 | )
44 | }
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/java/dev/lbeernaert/youhavemail/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail.ui.theme
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | body1 = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp
15 | )
16 | /* Other default text styles to override
17 | button = TextStyle(
18 | fontFamily = FontFamily.Default,
19 | fontWeight = FontWeight.W500,
20 | fontSize = 14.sp
21 | ),
22 | caption = TextStyle(
23 | fontFamily = FontFamily.Default,
24 | fontWeight = FontWeight.Normal,
25 | fontSize = 12.sp
26 | )
27 | */
28 | )
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/drawable-hdpi/ic_stat_alert.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/drawable-hdpi/ic_stat_alert.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/drawable-hdpi/ic_stat_err.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/drawable-hdpi/ic_stat_err.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/drawable-hdpi/ic_stat_sync.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/drawable-hdpi/ic_stat_sync.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/drawable-mdpi/ic_stat_alert.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/drawable-mdpi/ic_stat_alert.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/drawable-mdpi/ic_stat_err.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/drawable-mdpi/ic_stat_err.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/drawable-mdpi/ic_stat_sync.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/drawable-mdpi/ic_stat_sync.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/drawable-xhdpi/ic_stat_alert.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/drawable-xhdpi/ic_stat_alert.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/drawable-xhdpi/ic_stat_err.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/drawable-xhdpi/ic_stat_err.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/drawable-xhdpi/ic_stat_sync.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/drawable-xhdpi/ic_stat_sync.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/drawable-xxhdpi/ic_stat_alert.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/drawable-xxhdpi/ic_stat_alert.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/drawable-xxhdpi/ic_stat_err.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/drawable-xxhdpi/ic_stat_err.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/drawable-xxhdpi/ic_stat_sync.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/drawable-xxhdpi/ic_stat_sync.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/drawable-xxxhdpi/ic_stat_alert.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/drawable-xxxhdpi/ic_stat_alert.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/drawable-xxxhdpi/ic_stat_err.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/drawable-xxxhdpi/ic_stat_err.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/drawable-xxxhdpi/ic_stat_sync.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/drawable-xxxhdpi/ic_stat_sync.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/drawable/ic_base.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
12 |
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #5F3DDC
4 |
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | You Have Mail
3 | Connecting to Service...
4 | No active accounts.
5 | Add Account
6 | Username
7 | Password
8 | Log into %1$s
9 | Login
10 | Submit
11 | TOTP
12 | Please type in TOTP verification code
13 | Account Successfully added
14 | Select Backend
15 | Logging into %1$s
16 | Status
17 | Status: %1$s
18 | Result: %1$s
19 | Online
20 | Error
21 | Offline
22 | Logged Out
23 | Submitting TOTP
24 | Logout
25 | Logging Out
26 | Delete Account
27 | Account Details
28 | Notification Permission
29 |
30 | Notification permission is required, to show notification.
31 |
32 |
33 | Notification permission is required, Please allow notification permission from settings.
34 |
35 | Cancel
36 | Ok
37 | You Have Mail Alert
38 | %1$s has %2$d new messages
39 | %1$s encountered an error "%2$s"
40 | Settings
41 | Poll Interval
42 | Second
43 | Seconds
44 | Minute
45 | Minutes
46 | Updating Poll Interval
47 |
48 | Frequency at which the observer should poll the accounts. A higher poll interval can help
49 | reduce battery usage.
50 |
51 | Use Proxy
52 | Applying Proxy Settings...
53 | IP Address
54 | IP Port
55 | Proxy User
56 | Proxy Protocol
57 | Proxy Settings
58 | Proxy User Password
59 | Authentication
60 | This step is optional. If you don\'t normally use a Proxy to connect or don\'t know what a Proxy is, click Next to continue.
61 | Next
62 | Apply
63 | Activity
64 | Captcha Verification
65 | Retrying Login after Captcha Solve
66 | Export Logs
67 | Logs successfully exported
68 | Failed to export logs: %1$s
69 | Email and password fields cannot be empty
70 | E-mail address seems to be invalid. Verify if it is correctly typed.
71 | Service failed to initialize.
72 | Message marked as read
73 | Failed to mark message as read
74 | Message moved to Trash
75 | Failed to move message to Trash
76 | Message moved to Spam
77 | Failed to move message to Spam
78 | Action Succeeded
79 | Action Failed
80 | Mark Read
81 | Trash
82 | Spam
83 |
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/you-have-mail-android/app/src/test/java/dev/lbeernaert/youhavemail/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package dev.lbeernaert.youhavemail
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/you-have-mail-android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext {
3 | compose_ui_version = '1.6.8'
4 | versions_work = "2.9.0"
5 | }
6 | }// Top-level build file where you can add configuration options common to all sub-projects/modules.
7 |
8 | plugins {
9 | id 'com.android.application' version '8.1.4' apply false
10 | id 'com.android.library' version '8.1.4' apply false
11 | id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
12 | id "org.mozilla.rust-android-gradle.rust-android" version "0.9.5"
13 | }
--------------------------------------------------------------------------------
/you-have-mail-android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 | android.defaults.buildfeatures.buildconfig=true
25 | android.nonFinalResIds=false
--------------------------------------------------------------------------------
/you-have-mail-android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LeanderBB/you-have-mail/10dffc986fe50cd594fbc0cdefe6eab9327944a9/you-have-mail-android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/you-have-mail-android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Mar 18 11:30:03 CET 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/you-have-mail-android/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/you-have-mail-android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/you-have-mail-android/icon_inkscape.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
90 |
--------------------------------------------------------------------------------
/you-have-mail-android/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "YouHaveMail"
16 | include ':app'
17 |
--------------------------------------------------------------------------------
/you-have-mail-mobile/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "you-have-mail-mobile"
3 | version = "0.14.0"
4 | edition = "2024"
5 | authors = ["Leander Beernaert "]
6 | license = "AGPL-3.0-only"
7 | categories = ["email", "api-bindings"]
8 | description = "You-Have-Mail mobile API bindings"
9 | rust-version= "1.85.0"
10 |
11 | [lib]
12 | crate-type = ["cdylib"]
13 | name = "youhavemail"
14 |
15 | [dependencies]
16 | uniffi = { version = "0.29.0", features = ["cli"] }
17 | you-have-mail-common = { git = "https://github.com/LeanderBB/you-have-mail-common" }
18 | #you-have-mail-common = { path = "../../you-have-mail-common/youhavemail" }
19 | thiserror = "2"
20 | parking_lot = "0.12"
21 | tracing = "0.1"
22 | tracing-subscriber = { version = "0.3", features = ["env-filter"] }
23 | tracing-appender = "0.2.3"
24 | chrono = "0.4"
25 | sqlite-watcher = "0.4.1"
26 |
27 | [dev-dependencies]
28 | tempdir = "0.3.7"
29 |
30 | [lints.clippy]
31 | pedantic = "deny"
32 |
33 | [[bin]]
34 | # This can be whatever name makes sense for your project, but the rest of this tutorial assumes uniffi-bindgen.
35 | name = "uniffi-bindgen"
36 | path = "uniffi-bindgen/uniffi-bindgen.rs"
37 |
38 |
39 | [profile.release]
40 | strip = true
41 | opt-level = "z" # Optimize for size.
42 | lto = true
43 | codegen-units = 1
44 | panic = "abort"
45 |
--------------------------------------------------------------------------------
/you-have-mail-mobile/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | channel = "1.85.0"
3 | profile = "default"
4 | targets = [
5 | "aarch64-linux-android",
6 | "armv7-linux-androideabi",
7 | "x86_64-linux-android",
8 | ]
9 |
--------------------------------------------------------------------------------
/you-have-mail-mobile/src/account.rs:
--------------------------------------------------------------------------------
1 | use crate::events::Event;
2 | use chrono::{DateTime, Local};
3 | use std::fmt::Debug;
4 | use std::sync::Arc;
5 | use you_have_mail_common as yhm;
6 |
7 | /// An account in the system.
8 | #[derive(uniffi::Object)]
9 | pub struct Account {
10 | account: yhm::state::Account,
11 | }
12 |
13 | #[uniffi::export]
14 | impl Account {
15 | pub fn email(&self) -> String {
16 | self.account.email().to_owned()
17 | }
18 |
19 | pub fn backend(&self) -> String {
20 | self.account.backend().to_string()
21 | }
22 |
23 | pub fn set_proxy(
24 | &self,
25 | proxy: Option,
26 | ) -> Result<(), crate::yhm::YhmError> {
27 | let proxy = proxy.map(Into::into);
28 | Ok(self
29 | .account
30 | .set_proxy(proxy.as_ref())
31 | .map_err(yhm::yhm::Error::from)?)
32 | }
33 |
34 | pub fn proxy(&self) -> Result