diff --git a/README.md b/README.md
index 5e1f101..00fd342 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
> ⚠️ **Early Alpha**: SUP is under rapid development. The Android app is being actively developed and the current version is not thoroughly tested. There are no stable releases yet. Use at your own risk.
-SUP is a UnifiedPush distributor that routes push notifications through Signal, allowing you to receive app notifications without exposing unique network fingerprints to any network observers. All notification traffic appears as regular Signal messages.
+SUP is a [UnifiedPush](https://unifiedpush.org/) server and distributor that routes push notifications through Signal, allowing you to receive app notifications without exposing unique network fingerprints to any network observers. All notification traffic appears as regular Signal messages.
## Why?
@@ -24,6 +24,12 @@ SUP also includes an optional Proton Mail integration, allowing you to receive e
Note that you'll need to run SUP on your own server (either at home or on a VPS) since it uses your personal Signal and Proton Mail credentials. A Raspberry Pi, which is a small and affordable micro-computer, works perfectly for this, using minimal power (3-5W) while running SUP 24/7.
+## How?
+
+SUP functions as a UnifiedPush server to proxy http-based requests to Signal groups via [signal-cli](https://github.com/AsamK/signal-cli).
+
+For the optional ProtonMail integration, SUP requires a server that runs Proton's official [proton-bridge](https://github.com/ProtonMail/proton-bridge). SUP's docker compose process will run an image from [protonmail-bridge-docker](https://github.com/shenxn/protonmail-bridge-docker). Once authenticated, the communication between SUP and proton-bridge will be over IMAP.
+
## Setup
### 1. Install Android App (Optional)
@@ -55,20 +61,20 @@ curl -L -O https://raw.githubusercontent.com/lone-cloud/sup/master/docker-compos
docker compose run --rm protonmail-bridge init
```
-2. **Login to Proton Mail Bridge**:
+2.**Login to Proton Mail Bridge**:
- At the `>>>` prompt, run: `login`
- Enter your email
- Enter your password
- Enter your 2FA code
-3. **Get IMAP credentials**:
+3.**Get IMAP credentials**:
- Run: `info`
- Copy the Username and Password shown
- Run: `exit` to quit
-4. **Add credentials to .env**:
+4.**Add credentials to .env**:
```bash
# Add these to your .env file
@@ -76,7 +82,7 @@ PROTON_IMAP_USERNAME=bridge-username-from-info-command
PROTON_IMAP_PASSWORD=bridge-generated-password-from-info-command
```
-5. **Start all services with Proton Mail**:
+5.**Start all services with Proton Mail**:
```bash
docker compose --profile protonmail up -d
@@ -84,7 +90,7 @@ docker compose --profile protonmail up -d
Your phone will now receive Signal notifications when Proton Mail receives new emails.
-Note that the bridge will first need to sync all of your old emails before you can start getting new email notifications.
+Note that the bridge will first need to sync all of your old emails before you can start getting new email notifications which may take a while, but this is a one-time setup.
### 3. SUP Server integration
@@ -102,10 +108,31 @@ nano .env
# Start SUP server
docker compose up -d
-# Link your Signal account (one-time setup)
-# Visit http://localhost:8080 and scan QR code with Signal app
```
+### 4. Configuration
+
+Link your Signal account (one-time setup)
+Visit localhost:8080 and scan QR code with Signal app
+
+#### Authenticate with the API_KEY
+
+
+
+#### Scan the QR code from your Signal app by linking SUP as a device
+
+
+
+#### A healthy setup with a linked account
+
+
+
+#### A healthy setup with a linked account and the optional Proton Mail integration
+
+
+
+
+
### Development
For local development, install Bun and signal-cli:
@@ -118,6 +145,8 @@ git clone https://github.com/lone-cloud/sup.git
cd sup
bun install
+cd server
+bun start
```
Then build and run with docker-compose.dev.yml:
@@ -132,13 +161,6 @@ or just the proton-bridge:
docker compose -f docker-compose.dev.yml up protonmail-bridge
```
-Or run services directly with Bun:
-
-```bash
-bun install
-bun --filter sup-server dev
-```
-
## Real-World Examples
### Proton Mail Notifications
@@ -173,7 +195,14 @@ Reboot your Home Assistant system and you'll then be able to send Signal notific

-SUP consists of two services that **MUST run together on the same machine**:
+SUP consists of two services that **MUST rSUP uses [basic access authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) for authentication. un together on the same machin`API_KEY` as a password and the username can be ignored.
+
+For API-based monitoring, you can call `/api/health` which will return JSON health output like:
+
+```JSON
+{"uptime":"3s","signal":{"daemon":"running","linked":true},"protonMail":"connected"}
+```
+**:
- **sup-server** (Bun): Receives webhooks, sends Signal messages via signal-cli. Optional: monitors Proton Mail IMAP
- **protonmail-bridge** (Official Proton, optional): Decrypts Proton Mail emails, runs local IMAP server
diff --git a/assets/screenshots/1.webp b/assets/screenshots/1.webp
new file mode 100644
index 0000000..18287eb
Binary files /dev/null and b/assets/screenshots/1.webp differ
diff --git a/assets/screenshots/2.webp b/assets/screenshots/2.webp
new file mode 100644
index 0000000..04ad05e
Binary files /dev/null and b/assets/screenshots/2.webp differ
diff --git a/assets/screenshots/3.webp b/assets/screenshots/3.webp
new file mode 100644
index 0000000..ae5f67a
Binary files /dev/null and b/assets/screenshots/3.webp differ
diff --git a/assets/screenshots/4.webp b/assets/screenshots/4.webp
new file mode 100644
index 0000000..cf3474c
Binary files /dev/null and b/assets/screenshots/4.webp differ
diff --git a/server/.env.example b/server/.env.example
index 15481e7..9cba50b 100644
--- a/server/.env.example
+++ b/server/.env.example
@@ -1,11 +1,10 @@
+# Required: API key for authentication
+# API_KEY=your-secret-key-here
+
# Optional: Server port
# Default: 8080
# PORT=8080
-# Optional: Protect endpoint registration with an API key (recommended for public deployments)
-# Default: unset (no authentication required)
-# API_KEY=your-secret-key-here
-
# Optional: Allow HTTP connections without HTTPS
# Default: false
# ALLOW_INSECURE_HTTP=true
@@ -16,7 +15,7 @@
# Optional: Device name shown in Signal app's linked devices list
# Default: SUP
-# DEVICE_NAME=SUP dev
+# DEVICE_NAME=What SUP
# Optional: Enable verbose logging
# Default: false
diff --git a/server/modules/protonmail.ts b/server/modules/protonmail.ts
index d5b8939..0104494 100644
--- a/server/modules/protonmail.ts
+++ b/server/modules/protonmail.ts
@@ -11,6 +11,7 @@ import { getOrCreateGroup } from '@/modules/store';
import { logError, logInfo, logSuccess, logVerbose, logWarn } from '@/utils/log';
let imapConnected = false;
+let monitorStartTime = 0;
export const isImapConnected = () => imapConnected;
@@ -68,11 +69,18 @@ export async function startProtonMonitor() {
return;
}
- imap.on('mail', async (numNewMsgs: number) => {
- logVerbose(`${numNewMsgs} new message(s) received`);
+ if (!monitorStartTime) {
+ monitorStartTime = Date.now();
+ logVerbose(`Inbox opened with ${box.messages.total} existing messages`);
+ } else {
+ logVerbose('Inbox reopened (reconnection)');
+ }
- const fetch = imap.seq.fetch(`${box.messages.total}:*`, {
- bodies: 'HEADER.FIELDS (FROM SUBJECT)',
+ imap.on('mail', async (numNewMsgs: number) => {
+ logVerbose(`${numNewMsgs} new message(s) in mailbox`);
+
+ const fetch = imap.seq.fetch(`${box.messages.total - numNewMsgs + 1}:*`, {
+ bodies: 'HEADER.FIELDS (FROM SUBJECT DATE)',
struct: true,
});
@@ -86,6 +94,19 @@ export async function startProtonMonitor() {
const header = Imap.parseHeader(buffer);
const rawFrom = header.from?.[0] || 'Unknown sender';
const subject = header.subject?.[0] || 'No subject';
+ const dateStr = header.date?.[0];
+
+ if (dateStr) {
+ const messageDate = new Date(dateStr).getTime();
+ if (messageDate < monitorStartTime) {
+ const formattedDate = new Date(dateStr).toLocaleString('en-US', {
+ dateStyle: 'medium',
+ timeStyle: 'short',
+ });
+ logVerbose(`Skipping old email: ${subject} (${formattedDate})`);
+ return;
+ }
+ }
const nameMatch = rawFrom.match(/^"?([^"<]+)"?\s*/);
const from = nameMatch?.[1]?.trim() || rawFrom;
diff --git a/server/modules/signal.ts b/server/modules/signal.ts
index 64b0a07..70bb510 100644
--- a/server/modules/signal.ts
+++ b/server/modules/signal.ts
@@ -7,6 +7,7 @@ import {
} from '@/constants/config';
import { SIGNAL_CLI, SIGNAL_CLI_DATA, SIGNAL_CLI_SOCKET } from '@/constants/paths';
import type { ListAccountsResult, StartLinkResult, UpdateGroupResult } from '@/types';
+import { formatPhoneNumber } from '@/utils/format';
import { logError, logInfo, logSuccess, logVerbose, logWarn } from '@/utils/log';
import { call } from '@/utils/rpc';
@@ -34,7 +35,7 @@ export async function initSignal({ accountOverride }: { accountOverride?: string
const [firstAccount] = result;
if (firstAccount) {
account = firstAccount.number;
- logVerbose(`Signal account initialized: ${account}`);
+ logVerbose(`Signal account initialized: ${formatPhoneNumber(account)}`);
logSuccess('Signal account linked');
return { linked: true, account };
}
@@ -267,3 +268,5 @@ export async function startDaemon() {
}
export const cleanupDaemon = () => daemon?.kill();
+
+export const getAccount = () => account;
diff --git a/server/public/index.css b/server/public/index.css
index f94220a..24a6c61 100644
--- a/server/public/index.css
+++ b/server/public/index.css
@@ -72,6 +72,46 @@ h2 {
padding: 0.625rem 1rem;
border-radius: 0.25rem;
font-weight: 500;
+ position: relative;
+}
+
+.status-item:has(.tooltip) {
+ cursor: help;
+}
+
+.status-item .tooltip {
+ visibility: hidden;
+ opacity: 0;
+ position: absolute;
+ bottom: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ margin-bottom: 0.5rem;
+ background: #333;
+ color: white;
+ padding: 0.375rem 0.625rem;
+ border-radius: 0.25rem;
+ font-size: 0.875rem;
+ font-weight: 400;
+ white-space: nowrap;
+ transition: opacity 0.2s;
+ pointer-events: none;
+ z-index: 10;
+}
+
+.status-item .tooltip::after {
+ content: "";
+ position: absolute;
+ top: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ border: 0.3125rem solid transparent;
+ border-top-color: #333;
+}
+
+.status-item:hover .tooltip {
+ visibility: visible;
+ opacity: 1;
}
.status-ok {
@@ -186,6 +226,24 @@ h2 {
.loading {
color: #666;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.spinner {
+ width: 1.25rem;
+ height: 1.25rem;
+ border: 0.125rem solid #e0e0e0;
+ border-top-color: #8159b8;
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
}
.loading-subtitle {
diff --git a/server/public/index.html b/server/public/index.html
index 1436185..fa37cb4 100644
--- a/server/public/index.html
+++ b/server/public/index.html
@@ -13,24 +13,27 @@