diff --git a/EMAIL_UPLOAD.md b/EMAIL_UPLOAD.md new file mode 100644 index 0000000..bbe352d --- /dev/null +++ b/EMAIL_UPLOAD.md @@ -0,0 +1,83 @@ +# Email-to-Photo-Frame Upload + +Photos emailed to a dedicated mailbox are automatically saved to the +photo-frame Samba share, where the kiosk picks them up on its next scan +(PhotoManager rescans every 10 minutes). + +## Three moving parts + +### 1. Mailbox to receive photos + +Easiest path: a dedicated Gmail account (e.g. `deathstarhomephotos@gmail.com`). + +- Enable 2FA on the Google account +- Generate an **app password** under Security → App passwords (needed for IMAP, + since n8n's IMAP node uses username/password auth) +- Share the address with family members + +Lock it down in the n8n workflow by whitelisting sender addresses; reject +anything else so strangers can't spam the frame. + +### 2. Samba share visible to n8n + +n8n runs on 192.168.1.254 and needs to write to the same Samba share the +kiosk reads from. Two options: + +**Option A — Mount the share inside the n8n container** + +```bash +# On the n8n LXC host (192.168.1.254), install cifs-utils and mount: +apt install -y cifs-utils +mkdir -p /mnt/family-photos +echo '//SERVER/SHARE /mnt/family-photos cifs credentials=/etc/samba/credentials-n8n,uid=1000,gid=1000,rw,_netdev,x-systemd.automount 0 0' >> /etc/fstab +# credentials file with chmod 600 +mount -a +``` + +Then bind-mount `/mnt/family-photos` into the n8n Docker container so the +Write Binary File node can target it at a path like `/data/family-photos/`. + +**Option B — SSH to the fileshare LXC (192.168.1.193)** + +Give n8n an SSH key to the fileshare host. Use the Execute Command node to +`scp` / `cat > file` onto the share. Simpler to configure than cifs, but +adds SSH surface area on the fileshare box. + +### 3. n8n workflow + +Pseudocode: + +``` +IMAP Email Trigger (mark read, download attachments, poll every 2 min) + → IF From ∈ [your-email, wife-email] + → Split attachments + → IF mimeType startsWith "image/" + → Rename: ${yyyyMMdd_HHmmss}_${randomHex}.${ext} + → Write Binary File → /data/family-photos/${filename} +``` + +Nodes used: +- **Email Trigger (IMAP)** — reads Gmail (server `imap.gmail.com`, port 993, SSL) +- **IF** — sender whitelist guard +- **Item Lists / Split In Batches** — one item per attachment +- **IF** — filter on `$binary.attachment_0.mimeType` +- **Set** — build filename +- **Write Binary File** — write to the mount path + +Kiosk picks them up on the next rescan (up to 10 min), or restart the app +to force an immediate scan. + +## Hardening + +- Limit attachment size (e.g. reject > 30 MB per file) to avoid filling + the share +- Strip EXIF if you don't want location data persisted on the share +- Keep an audit trail: add an extra branch that appends sender + filename + to a log file + +## Credentials I'll need to build the workflow + +- Gmail address + app password (or OAuth refresh token) +- Which option above for the Samba mount (A or B) +- If option B, SSH key + fileshare path to write to +- List of allowed sender emails diff --git a/electron/main.ts b/electron/main.ts index 94c465a..58197f1 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,6 +1,7 @@ import { app, BrowserWindow, ipcMain, screen, powerSaveBlocker } from 'electron'; import * as path from 'path'; import * as fs from 'fs'; +import * as http from 'http'; import { ScreenManager } from './services/ScreenManager'; import { PresenceDetector } from './services/PresenceDetector'; import { FrigateStreamer } from './services/FrigateStreamer'; @@ -230,6 +231,34 @@ app.on('window-all-closed', () => { } }); +// Localhost-only HTTP endpoint for self-screenshots. GNOME Wayland blocks +// external screen capture, but webContents.capturePage() works from inside. +// Usage: curl -s http://127.0.0.1:9990/screenshot > out.png +http + .createServer(async (req, res) => { + if (req.url === '/screenshot' && mainWindow) { + try { + const wc = mainWindow.webContents; + if (!wc.debugger.isAttached()) wc.debugger.attach('1.3'); + const result = (await wc.debugger.sendCommand('Page.captureScreenshot', { + format: 'png', + captureBeyondViewport: false, + })) as { data: string }; + res.setHeader('Content-Type', 'image/png'); + res.end(Buffer.from(result.data, 'base64')); + } catch (err) { + res.statusCode = 500; + res.end(String(err)); + } + } else { + res.statusCode = 404; + res.end(); + } + }) + .listen(9990, '127.0.0.1', () => { + console.log('Screenshot endpoint on http://127.0.0.1:9990/screenshot'); + }); + // Handle uncaught exceptions process.on('uncaughtException', (error) => { console.error('Uncaught exception:', error);