Compare commits
36 Commits
Author | SHA1 | Date |
---|---|---|
Jarno Rankinen | d4213d273b | |
Jarno Rankinen | f2e7d4b499 | |
Jarno Rankinen | bbb21d59c7 | |
Jarno Rankinen | 25b6965e5e | |
Jarno Rankinen | e79a8e3f65 | |
Jarno Rankinen | f63b02a0d0 | |
Jarno Rankinen | d655c9caea | |
Jarno Rankinen | ea646a404b | |
Jarno Rankinen | b9be8b529f | |
Jarno Rankinen | 216b77e0ef | |
Jarno Rankinen | 54e545a1b5 | |
Jarno Rankinen | 544eb93e05 | |
Jarno Rankinen | cb3c36c6ea | |
Jarno Rankinen | 377395a532 | |
Jarno Rankinen | 375d7fc0b7 | |
Jarno Rankinen | 8a6a9e9380 | |
Jarno Rankinen | 65a6020623 | |
Jarno Rankinen | 58ce783fcb | |
Jarno Rankinen | 27bff1d3a1 | |
Jarno Rankinen | 234327cdf6 | |
Jarno Rankinen | 3e509a11b6 | |
Jarno Rankinen | 69ba3adb80 | |
Jarno Rankinen | 23cb629f09 | |
Jarno Rankinen | 94efe4ea80 | |
Jarno Rankinen | 3512aa9530 | |
Jarno Rankinen | 6c18227d0d | |
Jarno Rankinen | 1f0f33ee50 | |
Jarno Rankinen | 14056ff639 | |
Jarno Rankinen | f66ce030cf | |
Jarno Rankinen | 26dc3fa6fc | |
Jarno Rankinen | d4451a8a15 | |
Jarno Rankinen | 308eec6c69 | |
Simon Ser | 15fced74e9 | |
Simon Ser | 04ec4932f4 | |
Simon Ser | 54ba20031e | |
Zane Dufour | bcbbd4d533 |
|
@ -0,0 +1,28 @@
|
||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/go
|
||||||
|
{
|
||||||
|
"name": "Go",
|
||||||
|
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/go:1-1.22-bookworm",
|
||||||
|
|
||||||
|
"remoteUser": "vscode",
|
||||||
|
"containerUser": "vscode",
|
||||||
|
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,Z",
|
||||||
|
"workspaceFolder": "/workspace",
|
||||||
|
"runArgs": ["--userns=keep-id"]
|
||||||
|
|
||||||
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
|
// "features": {},
|
||||||
|
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
// "forwardPorts": [],
|
||||||
|
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
// "postCreateCommand": "go version",
|
||||||
|
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
// "customizations": {},
|
||||||
|
|
||||||
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
// "remoteUser": "root"
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
buildBinary:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '1.22.1'
|
||||||
|
- name: Build release binaries
|
||||||
|
uses: goreleaser/goreleaser-action@v5
|
||||||
|
with:
|
||||||
|
distribution: goreleaser
|
||||||
|
version: latest
|
||||||
|
args: release --clean
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
buildContainer:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Login to ghcr.io
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- uses: docker/build-push-action@v5
|
||||||
|
name: Build container image
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/arm
|
||||||
|
tags: |
|
||||||
|
ghcr.io/0ranki/hydroxide-push:latest
|
|
@ -0,0 +1,33 @@
|
||||||
|
version: 1
|
||||||
|
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
|
||||||
|
builds:
|
||||||
|
- main: ./cmd/hydroxide-push
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
- arm
|
||||||
|
|
||||||
|
archives:
|
||||||
|
- format: binary
|
||||||
|
name_template: >-
|
||||||
|
{{ .ProjectName }}-
|
||||||
|
{{- .Os }}-
|
||||||
|
{{- if eq .Arch "amd64" }}x86_64
|
||||||
|
{{- else if eq .Arch "386" }}i386
|
||||||
|
{{- else }}{{ .Arch }}{{ end }}
|
||||||
|
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
sort: asc
|
||||||
|
filters:
|
||||||
|
exclude:
|
||||||
|
- "^docs:"
|
||||||
|
- "^test:"
|
||||||
|
- "^ci:"
|
|
@ -0,0 +1,13 @@
|
||||||
|
FROM golang:1.22 as builder
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . /src
|
||||||
|
RUN CGO_ENABLED=0 go build -o /hydroxide-push ./cmd/hydroxide-push/
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
VOLUME /data
|
||||||
|
COPY --from=builder /hydroxide-push /
|
||||||
|
ENV HOME=/data
|
||||||
|
WORKDIR /data
|
||||||
|
ENTRYPOINT ["/hydroxide-push"]
|
||||||
|
CMD ["notify"]
|
185
README.md
185
README.md
|
@ -1,104 +1,127 @@
|
||||||
# hydroxide
|
# hydroxide-push
|
||||||
|
### *Forked from [Hydroxide](https://github.com/emersion/hydroxide)*
|
||||||
|
[![.github/workflows/build.yaml](https://github.com/0ranki/hydroxide-push/actions/workflows/build.yaml/badge.svg)](https://github.com/0ranki/hydroxide-push/actions/workflows/build.yaml) ![GitHub Release](https://img.shields.io/github/v/release/0ranki/hydroxide-push)
|
||||||
|
|
||||||
A third-party, open-source ProtonMail bridge. For power users only, designed to
|
<img src="https://github.com/0ranki/hydroxide-push/assets/50285623/04959566-3d13-4be4-84bd-d7daad3a3166" width="600">
|
||||||
run on a server.
|
|
||||||
|
|
||||||
hydroxide supports CardDAV, IMAP and SMTP.
|
## Push notifications for Proton Mail mobile via a UP provider
|
||||||
|
|
||||||
Rationale:
|
Protonmail depends on Google services to deliver push notifications,
|
||||||
|
This is a stripped down version of [Hydroxide](https://github.com/emersion/hydroxide)
|
||||||
|
to get notified of new mail. See original repo for details on operation.
|
||||||
|
|
||||||
* No GUI, only a CLI (so it runs in headless environments)
|
Should work with free accounts too.
|
||||||
* Standard-compliant (we don't care about Microsoft Outlook)
|
|
||||||
* Fully open-source
|
|
||||||
|
|
||||||
Feel free to join the IRC channel: #emersion on Libera Chat.
|
<sup><sub>Github is used to build the binaries and container images with Github Actions, and host the pre-built releases.
|
||||||
|
Mirrored from https://git.oranki.net/jarno/hydroxide-push</sub></sup>
|
||||||
|
|
||||||
## How does it work?
|
Pre-built releases and container images are available on [Github](https://github.com/0ranki/hydroxide-push).
|
||||||
|
|
||||||
hydroxide is a server that translates standard protocols (SMTP, IMAP, CardDAV)
|
|
||||||
into ProtonMail API requests. It allows you to use your preferred e-mail clients
|
|
||||||
and `git-send-email` with ProtonMail.
|
|
||||||
|
|
||||||
+-----------------+ +-------------+ ProtonMail +--------------+
|
|
||||||
| | IMAP, SMTP | | API | |
|
|
||||||
| E-mail client <-------------> hydroxide <--------------> ProtonMail |
|
|
||||||
| | | | | |
|
|
||||||
+-----------------+ +-------------+ +--------------+
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
### Go
|
Download or build the binary, pull the pre-built container image or build the image yourself.
|
||||||
|
Simplest way is to run the pre-built container image.
|
||||||
|
|
||||||
hydroxide is implemented in Go. Head to [Go website](https://golang.org) for
|
Login and push gateway details are saved under `$HOME/.config/hydroxide`. The container
|
||||||
setup information.
|
image saves configuration under `/data`, so mount a named volume or host directory there.
|
||||||
|
The examples below use a named volume.
|
||||||
|
|
||||||
### Installing
|
If using Docker, substitute `podman` with `docker` in the examples.
|
||||||
|
|
||||||
Start by installing hydroxide:
|
|
||||||
|
|
||||||
|
Binary:
|
||||||
```shell
|
```shell
|
||||||
git clone https://github.com/emersion/hydroxide.git
|
./hydroxide-push auth your.proton@email.address
|
||||||
go build ./cmd/hydroxide
|
```
|
||||||
|
Container:
|
||||||
|
```shell
|
||||||
|
podman run -it --rm -v hydroxide-push:/data ghcr.io/0ranki/hydroxide-push auth your.proton@email.address
|
||||||
|
```
|
||||||
|
You will be prompted for the Proton account credentials and the details for the push server. Proton credentials are stored encrypted form.
|
||||||
|
|
||||||
|
The auth flow generates a separate password for the bridge to fake a login to the bridge, which is stored in plaintext to `$HOME/.config/notify.json`. Unlike upstream `hydroxide`, there is no service listening on any port, the password isn't useful for anything else.
|
||||||
|
|
||||||
|
### Reconfigure push server
|
||||||
|
Binary:
|
||||||
|
```shell
|
||||||
|
hydroxide-push setup-ntfy
|
||||||
|
```
|
||||||
|
Container:
|
||||||
|
```shell
|
||||||
|
podman run -it --rm -v hydroxide-push:/data ghcr.io/0ranki/hydroxide-push setup-ntfy
|
||||||
|
```
|
||||||
|
Alternatively to run the command in an already running container (replace `name-of-container` with the name or id of the running container):
|
||||||
|
```shell
|
||||||
|
podman exec -it name-of-container /hydroxide-push setup-ntfy
|
||||||
|
```
|
||||||
|
You'll be asked for the base URL of the push server, topic then username and password for HTTP basic authentication.
|
||||||
|
The push endpoint configuration can be changed while the daemon is running.
|
||||||
|
|
||||||
|
Username and password are stored in `notify.json`, the password is only Base64-encoded. You should create a dedicated user that
|
||||||
|
has write-only access to the topic for the daemon.
|
||||||
|
|
||||||
|
**Push topic username and password are cleared each time setup-ntfy is run, they need to be entered manually every time.**
|
||||||
|
|
||||||
|
The currently configured values are shown inside braces. Leave input blank to use the current values.
|
||||||
|
|
||||||
|
### Start the service
|
||||||
|
|
||||||
|
Binary:
|
||||||
|
```shell
|
||||||
|
hydroxide-push notify
|
||||||
|
```
|
||||||
|
Container:
|
||||||
|
```shell
|
||||||
|
podman run -it --rm -v hydroxide-push:/data ghcr.io/0ranki/hydroxide-push
|
||||||
```
|
```
|
||||||
|
|
||||||
Then you'll need to login to ProtonMail via hydroxide, so that hydroxide can
|
## Podman pod
|
||||||
retrieve e-mails from ProtonMail. You can do so with this command:
|
|
||||||
|
|
||||||
|
A Podman kube YAML file is provided in the repo.
|
||||||
|
|
||||||
|
> **Note:** If you're using 2FA or just don't want to put your password to a file, use the manual method above. Make sure the volume name (claimName) in the YAML mathces what you use in the commands.
|
||||||
|
|
||||||
|
- Download/copy `hydroxide-push-podman.yaml` to an empty directory on the machine you intend to run the daemon on
|
||||||
|
- Edit the config values at the top of the file
|
||||||
|
- Start the pod:
|
||||||
|
```shell
|
||||||
|
podman kube play ./hydroxide-push-podman.yaml
|
||||||
|
```
|
||||||
|
- Latest container image is pulled
|
||||||
|
- A named volume (`hydroxide-push`) will be created for the configuration
|
||||||
|
- Login to Proton and push URL configuration is handled automatically, after which the daemon starts
|
||||||
|
- After the initial setup, the ConfigMap (before `---`) can be removed from the YAML. Optionally to clear the environment variables, run
|
||||||
|
|
||||||
|
```shell
|
||||||
|
podman kube play ./hydroxide-push-podman.yaml --replace
|
||||||
|
```
|
||||||
|
The command can also be used to pull the latest version and restart the pod.
|
||||||
|
- To reauthenticate or clear data, simply remove the named volume or run the `auth` command
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
Binary:
|
||||||
|
- stop the service
|
||||||
|
- download or build the new version, replace the of the old binary
|
||||||
|
- restart the service
|
||||||
|
|
||||||
|
Container:
|
||||||
|
- pull latest image
|
||||||
|
- restart container
|
||||||
|
|
||||||
|
## Building locally
|
||||||
|
Clone the repo, then `cd` to the repo root
|
||||||
|
|
||||||
|
Binary:
|
||||||
|
> Requires Go 1.22
|
||||||
```shell
|
```shell
|
||||||
hydroxide auth <username>
|
CGO_ENABLED=0 go build -o $HOME/.local/bin/hydroxide-push ./cmd/hydroxide-push/
|
||||||
|
```
|
||||||
|
Container:
|
||||||
|
```shell
|
||||||
|
podman build -t localhost/hydroxide-push:latest .
|
||||||
```
|
```
|
||||||
|
|
||||||
Once you're logged in, a "bridge password" will be printed. Don't close your
|
|
||||||
terminal yet, as this password is not stored anywhere by hydroxide and will be
|
|
||||||
needed when configuring your e-mail client.
|
|
||||||
|
|
||||||
Your ProtonMail credentials are stored on disk encrypted with this bridge
|
|
||||||
password (a 32-byte random password generated when logging in).
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
hydroxide can be used in multiple modes.
|
|
||||||
|
|
||||||
> Don't start hydroxide multiple times, instead you can use `hydroxide serve`.
|
|
||||||
> This requires ports 1025 (smtp), 1143 (imap), and 8080 (carddav).
|
|
||||||
|
|
||||||
### SMTP
|
|
||||||
|
|
||||||
To run hydroxide as an SMTP server:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
hydroxide smtp
|
|
||||||
```
|
|
||||||
|
|
||||||
Once the bridge is started, you can configure your e-mail client with the
|
|
||||||
following settings:
|
|
||||||
|
|
||||||
* Hostname: `localhost`
|
|
||||||
* Port: 1025
|
|
||||||
* Security: none
|
|
||||||
* Username: your ProtonMail username
|
|
||||||
* Password: the bridge password (not your ProtonMail password)
|
|
||||||
|
|
||||||
### CardDAV
|
|
||||||
|
|
||||||
You must setup an HTTPS reverse proxy to forward requests to `hydroxide`.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
hydroxide carddav
|
|
||||||
```
|
|
||||||
|
|
||||||
Tested on GNOME (Evolution) and Android (DAVDroid).
|
|
||||||
|
|
||||||
### IMAP
|
|
||||||
|
|
||||||
⚠️ **Warning**: IMAP support is work-in-progress. Here be dragons.
|
|
||||||
|
|
||||||
For now, it only supports unencrypted local connections.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
hydroxide imap
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|
|
@ -13,8 +13,8 @@ import (
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"golang.org/x/crypto/nacl/secretbox"
|
"golang.org/x/crypto/nacl/secretbox"
|
||||||
|
|
||||||
"github.com/emersion/hydroxide/config"
|
"github.com/0ranki/hydroxide-push/config"
|
||||||
"github.com/emersion/hydroxide/protonmail"
|
"github.com/0ranki/hydroxide-push/protonmail"
|
||||||
)
|
)
|
||||||
|
|
||||||
func authFilePath() (string, error) {
|
func authFilePath() (string, error) {
|
||||||
|
|
|
@ -13,10 +13,10 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/0ranki/hydroxide-push/protonmail"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp"
|
"github.com/ProtonMail/go-crypto/openpgp"
|
||||||
"github.com/emersion/go-vcard"
|
"github.com/emersion/go-vcard"
|
||||||
"github.com/emersion/go-webdav/carddav"
|
"github.com/emersion/go-webdav/carddav"
|
||||||
"github.com/emersion/hydroxide/protonmail"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: use a HTTP error
|
// TODO: use a HTTP error
|
||||||
|
@ -388,5 +388,5 @@ func NewHandler(c *protonmail.Client, privateKeys openpgp.EntityList, events <-c
|
||||||
go b.receiveEvents(events)
|
go b.receiveEvents(events)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &carddav.Handler{b}
|
return &carddav.Handler{Backend: b}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
.env
|
|
@ -0,0 +1,270 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"crypto/tls"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/0ranki/hydroxide-push/auth"
|
||||||
|
"github.com/0ranki/hydroxide-push/config"
|
||||||
|
"github.com/0ranki/hydroxide-push/events"
|
||||||
|
imapbackend "github.com/0ranki/hydroxide-push/imap"
|
||||||
|
"github.com/0ranki/hydroxide-push/ntfy"
|
||||||
|
"github.com/0ranki/hydroxide-push/protonmail"
|
||||||
|
imapserver "github.com/emersion/go-imap/server"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultAPIEndpoint = "https://mail.proton.me/api"
|
||||||
|
defaultAppVersion = "Other"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
debug bool
|
||||||
|
apiEndpoint string
|
||||||
|
appVersion string
|
||||||
|
cfg ntfy.NtfyConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
func newClient() *protonmail.Client {
|
||||||
|
return &protonmail.Client{
|
||||||
|
RootURL: apiEndpoint,
|
||||||
|
AppVersion: appVersion,
|
||||||
|
Debug: debug,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func askPass(prompt string) ([]byte, error) {
|
||||||
|
f := os.Stdin
|
||||||
|
if !term.IsTerminal(int(f.Fd())) {
|
||||||
|
// This can happen if stdin is used for piping data
|
||||||
|
// TODO: the following assumes Unix
|
||||||
|
var err error
|
||||||
|
if f, err = os.Open("/dev/tty"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "%v: ", prompt)
|
||||||
|
b, err := term.ReadPassword(int(f.Fd()))
|
||||||
|
if err == nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "\n")
|
||||||
|
}
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func listenEventsAndNotify(addr string, debug bool, authManager *auth.Manager, eventsManager *events.Manager, tlsConfig *tls.Config) {
|
||||||
|
be := imapbackend.New(authManager, eventsManager)
|
||||||
|
s := imapserver.New(be)
|
||||||
|
s.Addr = addr
|
||||||
|
s.AllowInsecureAuth = tlsConfig == nil
|
||||||
|
s.TLSConfig = tlsConfig
|
||||||
|
if debug {
|
||||||
|
s.Debug = os.Stdout
|
||||||
|
}
|
||||||
|
ntfy.Login(&cfg, be)
|
||||||
|
log.Println("Listening for events", s.Addr)
|
||||||
|
for {
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func authenticate(authCmd *flag.FlagSet) {
|
||||||
|
var username string
|
||||||
|
if os.Getenv("PROTON_ACCT") != "" {
|
||||||
|
username = os.Getenv("PROTON_ACCT")
|
||||||
|
} else {
|
||||||
|
username = authCmd.Arg(0)
|
||||||
|
}
|
||||||
|
if username == "" {
|
||||||
|
log.Fatal("usage: hydroxide auth <username>")
|
||||||
|
}
|
||||||
|
|
||||||
|
c := newClient()
|
||||||
|
|
||||||
|
var a *protonmail.Auth
|
||||||
|
/*if cachedAuth, ok := auths[username]; ok {
|
||||||
|
var err error
|
||||||
|
a, err = c.AuthRefresh(a)
|
||||||
|
if err != nil {
|
||||||
|
// TODO: handle expired token error
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
var loginPassword string
|
||||||
|
if a == nil {
|
||||||
|
if os.Getenv("PROTON_ACCT_PASSWORD") != "" {
|
||||||
|
loginPassword = os.Getenv("PROTON_ACCT_PASSWORD")
|
||||||
|
} else if pass, err := askPass("Password"); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
} else {
|
||||||
|
loginPassword = string(pass)
|
||||||
|
}
|
||||||
|
|
||||||
|
authInfo, err := c.AuthInfo(username)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a, err = c.Auth(username, loginPassword, authInfo)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.TwoFactor.Enabled != 0 {
|
||||||
|
if a.TwoFactor.TOTP != 1 {
|
||||||
|
log.Fatal("Only TOTP is supported as a 2FA method")
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
fmt.Printf("2FA TOTP code: ")
|
||||||
|
scanner.Scan()
|
||||||
|
code := scanner.Text()
|
||||||
|
|
||||||
|
scope, err := c.AuthTOTP(code)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
a.Scope = scope
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var mailboxPassword string
|
||||||
|
if a.PasswordMode == protonmail.PasswordSingle {
|
||||||
|
mailboxPassword = loginPassword
|
||||||
|
}
|
||||||
|
if mailboxPassword == "" {
|
||||||
|
prompt := "Password"
|
||||||
|
if a.PasswordMode == protonmail.PasswordTwo {
|
||||||
|
prompt = "Mailbox password"
|
||||||
|
}
|
||||||
|
if pass, err := askPass(prompt); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
} else {
|
||||||
|
mailboxPassword = string(pass)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keySalts, err := c.ListKeySalts()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.Unlock(a, keySalts, mailboxPassword)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secretKey, bridgePassword, err := auth.GeneratePassword()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = auth.EncryptAndSave(&auth.CachedAuth{
|
||||||
|
Auth: *a,
|
||||||
|
LoginPassword: loginPassword,
|
||||||
|
MailboxPassword: mailboxPassword,
|
||||||
|
KeySalts: keySalts,
|
||||||
|
}, username, secretKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
cfg.BridgePw = bridgePassword
|
||||||
|
cfg.Setup()
|
||||||
|
}
|
||||||
|
|
||||||
|
const usage = `usage: hydroxide-push [options...] <command>
|
||||||
|
Commands:
|
||||||
|
auth <username> Login to ProtonMail via hydroxide
|
||||||
|
status View hydroxide status
|
||||||
|
notify Start the notification daemon
|
||||||
|
setup-ntfy (Re)configure the push endpoint
|
||||||
|
|
||||||
|
Global options:
|
||||||
|
-debug
|
||||||
|
Enable debug logs
|
||||||
|
-api-endpoint <url>
|
||||||
|
ProtonMail API endpoint
|
||||||
|
-app-version <version>
|
||||||
|
ProtonMail application version
|
||||||
|
|
||||||
|
Environment variables:
|
||||||
|
HYDROXIDE_BRIDGE_PASS Don't prompt for the bridge password, use this variable instead
|
||||||
|
`
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.BoolVar(&debug, "debug", false, "Enable debug logs")
|
||||||
|
flag.StringVar(&apiEndpoint, "api-endpoint", defaultAPIEndpoint, "ProtonMail API endpoint")
|
||||||
|
flag.StringVar(&appVersion, "app-version", defaultAppVersion, "ProtonMail app version")
|
||||||
|
|
||||||
|
tlsCert := flag.String("tls-cert", "", "Path to the certificate to use for incoming connections")
|
||||||
|
tlsCertKey := flag.String("tls-key", "", "Path to the certificate key to use for incoming connections")
|
||||||
|
tlsClientCA := flag.String("tls-client-ca", "", "If set, clients must provide a certificate signed by the given CA")
|
||||||
|
|
||||||
|
authCmd := flag.NewFlagSet("auth", flag.ExitOnError)
|
||||||
|
|
||||||
|
flag.Usage = func() {
|
||||||
|
fmt.Print(usage)
|
||||||
|
}
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
tlsConfig, err := config.TLS(*tlsCert, *tlsCertKey, *tlsClientCA)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cfg.Read()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := flag.Arg(0)
|
||||||
|
switch cmd {
|
||||||
|
case "auth":
|
||||||
|
authCmd.Parse(flag.Args()[1:])
|
||||||
|
authenticate(authCmd)
|
||||||
|
|
||||||
|
case "status":
|
||||||
|
usernames, err := auth.ListUsernames()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(usernames) == 0 {
|
||||||
|
fmt.Printf("No logged in user.\n")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%v logged in user(s):\n", len(usernames))
|
||||||
|
for _, u := range usernames {
|
||||||
|
fmt.Printf("- %v\n", u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "setup-ntfy":
|
||||||
|
cfg.Setup()
|
||||||
|
|
||||||
|
case "notify":
|
||||||
|
if os.Getenv("PROTON_ACCT_PASSWORD") != "" && os.Getenv("PROTON_ACCT") != "" && os.Getenv("PUSH_URL") != "" && os.Getenv("PUSH_TOPIC") != "" && cfg.BridgePw == "" {
|
||||||
|
log.Println("Logging in to Proton account using values from environment")
|
||||||
|
cfg.URL = os.Getenv("PUSH_URL")
|
||||||
|
cfg.Topic = os.Getenv("PUSH_TOPIC")
|
||||||
|
cfg.Save()
|
||||||
|
authenticate(new(flag.FlagSet))
|
||||||
|
}
|
||||||
|
authManager := auth.NewManager(newClient)
|
||||||
|
eventsManager := events.NewManager()
|
||||||
|
listenEventsAndNotify("0", debug, authManager, eventsManager, tlsConfig)
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Print(usage)
|
||||||
|
if cmd != "help" {
|
||||||
|
log.Fatal("Unrecognized command")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,572 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"crypto/tls"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/ProtonMail/go-crypto/openpgp"
|
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
|
||||||
imapserver "github.com/emersion/go-imap/server"
|
|
||||||
"github.com/emersion/go-mbox"
|
|
||||||
"github.com/emersion/go-smtp"
|
|
||||||
"golang.org/x/term"
|
|
||||||
|
|
||||||
"github.com/emersion/hydroxide/auth"
|
|
||||||
"github.com/emersion/hydroxide/carddav"
|
|
||||||
"github.com/emersion/hydroxide/config"
|
|
||||||
"github.com/emersion/hydroxide/events"
|
|
||||||
"github.com/emersion/hydroxide/exports"
|
|
||||||
imapbackend "github.com/emersion/hydroxide/imap"
|
|
||||||
"github.com/emersion/hydroxide/imports"
|
|
||||||
"github.com/emersion/hydroxide/protonmail"
|
|
||||||
smtpbackend "github.com/emersion/hydroxide/smtp"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultAPIEndpoint = "https://mail.proton.me/api"
|
|
||||||
defaultAppVersion = "Other"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
debug bool
|
|
||||||
apiEndpoint string
|
|
||||||
appVersion string
|
|
||||||
)
|
|
||||||
|
|
||||||
func newClient() *protonmail.Client {
|
|
||||||
return &protonmail.Client{
|
|
||||||
RootURL: apiEndpoint,
|
|
||||||
AppVersion: appVersion,
|
|
||||||
Debug: debug,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func askPass(prompt string) ([]byte, error) {
|
|
||||||
f := os.Stdin
|
|
||||||
if !term.IsTerminal(int(f.Fd())) {
|
|
||||||
// This can happen if stdin is used for piping data
|
|
||||||
// TODO: the following assumes Unix
|
|
||||||
var err error
|
|
||||||
if f, err = os.Open("/dev/tty"); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
}
|
|
||||||
fmt.Fprintf(os.Stderr, "%v: ", prompt)
|
|
||||||
b, err := term.ReadPassword(int(f.Fd()))
|
|
||||||
if err == nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "\n")
|
|
||||||
}
|
|
||||||
return b, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func askBridgePass() (string, error) {
|
|
||||||
if v := os.Getenv("HYDROXIDE_BRIDGE_PASS"); v != "" {
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
b, err := askPass("Bridge password")
|
|
||||||
return string(b), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func listenAndServeSMTP(addr string, debug bool, authManager *auth.Manager, tlsConfig *tls.Config) error {
|
|
||||||
be := smtpbackend.New(authManager)
|
|
||||||
s := smtp.NewServer(be)
|
|
||||||
s.Addr = addr
|
|
||||||
s.Domain = "localhost" // TODO: make this configurable
|
|
||||||
s.AllowInsecureAuth = tlsConfig == nil
|
|
||||||
s.TLSConfig = tlsConfig
|
|
||||||
if debug {
|
|
||||||
s.Debug = os.Stdout
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.TLSConfig != nil {
|
|
||||||
log.Println("SMTP server listening with TLS on", s.Addr)
|
|
||||||
return s.ListenAndServeTLS()
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("SMTP server listening on", s.Addr)
|
|
||||||
return s.ListenAndServe()
|
|
||||||
}
|
|
||||||
|
|
||||||
func listenAndServeIMAP(addr string, debug bool, authManager *auth.Manager, eventsManager *events.Manager, tlsConfig *tls.Config) error {
|
|
||||||
be := imapbackend.New(authManager, eventsManager)
|
|
||||||
s := imapserver.New(be)
|
|
||||||
s.Addr = addr
|
|
||||||
s.AllowInsecureAuth = tlsConfig == nil
|
|
||||||
s.TLSConfig = tlsConfig
|
|
||||||
if debug {
|
|
||||||
s.Debug = os.Stdout
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.TLSConfig != nil {
|
|
||||||
log.Println("IMAP server listening with TLS on", s.Addr)
|
|
||||||
return s.ListenAndServeTLS()
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("IMAP server listening on", s.Addr)
|
|
||||||
return s.ListenAndServe()
|
|
||||||
}
|
|
||||||
|
|
||||||
func listenAndServeCardDAV(addr string, authManager *auth.Manager, eventsManager *events.Manager, tlsConfig *tls.Config) error {
|
|
||||||
handlers := make(map[string]http.Handler)
|
|
||||||
|
|
||||||
s := &http.Server{
|
|
||||||
Addr: addr,
|
|
||||||
TLSConfig: tlsConfig,
|
|
||||||
Handler: http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
|
||||||
resp.Header().Set("WWW-Authenticate", "Basic")
|
|
||||||
|
|
||||||
username, password, ok := req.BasicAuth()
|
|
||||||
if !ok {
|
|
||||||
resp.WriteHeader(http.StatusUnauthorized)
|
|
||||||
io.WriteString(resp, "Credentials are required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c, privateKeys, err := authManager.Auth(username, password)
|
|
||||||
if err != nil {
|
|
||||||
if err == auth.ErrUnauthorized {
|
|
||||||
resp.WriteHeader(http.StatusUnauthorized)
|
|
||||||
} else {
|
|
||||||
resp.WriteHeader(http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
io.WriteString(resp, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h, ok := handlers[username]
|
|
||||||
if !ok {
|
|
||||||
ch := make(chan *protonmail.Event)
|
|
||||||
eventsManager.Register(c, username, ch, nil)
|
|
||||||
h = carddav.NewHandler(c, privateKeys, ch)
|
|
||||||
|
|
||||||
handlers[username] = h
|
|
||||||
}
|
|
||||||
|
|
||||||
h.ServeHTTP(resp, req)
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.TLSConfig != nil {
|
|
||||||
log.Println("CardDAV server listening with TLS on", s.Addr)
|
|
||||||
return s.ListenAndServeTLS("", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("CardDAV server listening on", s.Addr)
|
|
||||||
return s.ListenAndServe()
|
|
||||||
}
|
|
||||||
|
|
||||||
func isMbox(br *bufio.Reader) (bool, error) {
|
|
||||||
prefix := []byte("From ")
|
|
||||||
b, err := br.Peek(len(prefix))
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return bytes.Equal(b, prefix), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const usage = `usage: hydroxide [options...] <command>
|
|
||||||
Commands:
|
|
||||||
auth <username> Login to ProtonMail via hydroxide
|
|
||||||
carddav Run hydroxide as a CardDAV server
|
|
||||||
export-secret-keys <username> Export secret keys
|
|
||||||
imap Run hydroxide as an IMAP server
|
|
||||||
import-messages <username> [file] Import messages
|
|
||||||
export-messages [options...] <username> Export messages
|
|
||||||
sendmail <username> -- <args...> sendmail(1) interface
|
|
||||||
serve Run all servers
|
|
||||||
smtp Run hydroxide as an SMTP server
|
|
||||||
status View hydroxide status
|
|
||||||
|
|
||||||
Global options:
|
|
||||||
-debug
|
|
||||||
Enable debug logs
|
|
||||||
-api-endpoint <url>
|
|
||||||
ProtonMail API endpoint
|
|
||||||
-app-version <version>
|
|
||||||
ProtonMail application version
|
|
||||||
-smtp-host example.com
|
|
||||||
Allowed SMTP email hostname on which hydroxide listens, defaults to 127.0.0.1
|
|
||||||
-imap-host example.com
|
|
||||||
Allowed IMAP email hostname on which hydroxide listens, defaults to 127.0.0.1
|
|
||||||
-carddav-host example.com
|
|
||||||
Allowed SMTP email hostname on which hydroxide listens, defaults to 127.0.0.1
|
|
||||||
-smtp-port example.com
|
|
||||||
SMTP port on which hydroxide listens, defaults to 1025
|
|
||||||
-imap-port example.com
|
|
||||||
IMAP port on which hydroxide listens, defaults to 1143
|
|
||||||
-carddav-port example.com
|
|
||||||
CardDAV port on which hydroxide listens, defaults to 8080
|
|
||||||
-disable-imap
|
|
||||||
Disable IMAP for hydroxide serve
|
|
||||||
-disable-smtp
|
|
||||||
Disable SMTP for hydroxide serve
|
|
||||||
-disable-carddav
|
|
||||||
Disable CardDAV for hydroxide serve
|
|
||||||
-tls-cert /path/to/cert.pem
|
|
||||||
Path to the certificate to use for incoming connections (Optional)
|
|
||||||
-tls-key /path/to/key.pem
|
|
||||||
Path to the certificate key to use for incoming connections (Optional)
|
|
||||||
-tls-client-ca /path/to/ca.pem
|
|
||||||
If set, clients must provide a certificate signed by the given CA (Optional)
|
|
||||||
|
|
||||||
Environment variables:
|
|
||||||
HYDROXIDE_BRIDGE_PASS Don't prompt for the bridge password, use this variable instead
|
|
||||||
`
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.BoolVar(&debug, "debug", false, "Enable debug logs")
|
|
||||||
flag.StringVar(&apiEndpoint, "api-endpoint", defaultAPIEndpoint, "ProtonMail API endpoint")
|
|
||||||
flag.StringVar(&appVersion, "app-version", defaultAppVersion, "ProtonMail app version")
|
|
||||||
|
|
||||||
smtpHost := flag.String("smtp-host", "127.0.0.1", "Allowed SMTP email hostname on which hydroxide listens, defaults to 127.0.0.1")
|
|
||||||
smtpPort := flag.String("smtp-port", "1025", "SMTP port on which hydroxide listens, defaults to 1025")
|
|
||||||
disableSMTP := flag.Bool("disable-smtp", false, "Disable SMTP for hydroxide serve")
|
|
||||||
|
|
||||||
imapHost := flag.String("imap-host", "127.0.0.1", "Allowed IMAP email hostname on which hydroxide listens, defaults to 127.0.0.1")
|
|
||||||
imapPort := flag.String("imap-port", "1143", "IMAP port on which hydroxide listens, defaults to 1143")
|
|
||||||
disableIMAP := flag.Bool("disable-imap", false, "Disable IMAP for hydroxide serve")
|
|
||||||
|
|
||||||
carddavHost := flag.String("carddav-host", "127.0.0.1", "Allowed CardDAV email hostname on which hydroxide listens, defaults to 127.0.0.1")
|
|
||||||
carddavPort := flag.String("carddav-port", "8080", "CardDAV port on which hydroxide listens, defaults to 8080")
|
|
||||||
disableCardDAV := flag.Bool("disable-carddav", false, "Disable CardDAV for hydroxide serve")
|
|
||||||
|
|
||||||
tlsCert := flag.String("tls-cert", "", "Path to the certificate to use for incoming connections")
|
|
||||||
tlsCertKey := flag.String("tls-key", "", "Path to the certificate key to use for incoming connections")
|
|
||||||
tlsClientCA := flag.String("tls-client-ca", "", "If set, clients must provide a certificate signed by the given CA")
|
|
||||||
|
|
||||||
authCmd := flag.NewFlagSet("auth", flag.ExitOnError)
|
|
||||||
exportSecretKeysCmd := flag.NewFlagSet("export-secret-keys", flag.ExitOnError)
|
|
||||||
importMessagesCmd := flag.NewFlagSet("import-messages", flag.ExitOnError)
|
|
||||||
exportMessagesCmd := flag.NewFlagSet("export-messages", flag.ExitOnError)
|
|
||||||
sendmailCmd := flag.NewFlagSet("sendmail", flag.ExitOnError)
|
|
||||||
|
|
||||||
flag.Usage = func() {
|
|
||||||
fmt.Print(usage)
|
|
||||||
}
|
|
||||||
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
tlsConfig, err := config.TLS(*tlsCert, *tlsCertKey, *tlsClientCA)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := flag.Arg(0)
|
|
||||||
switch cmd {
|
|
||||||
case "auth":
|
|
||||||
authCmd.Parse(flag.Args()[1:])
|
|
||||||
username := authCmd.Arg(0)
|
|
||||||
if username == "" {
|
|
||||||
log.Fatal("usage: hydroxide auth <username>")
|
|
||||||
}
|
|
||||||
|
|
||||||
c := newClient()
|
|
||||||
|
|
||||||
var a *protonmail.Auth
|
|
||||||
/*if cachedAuth, ok := auths[username]; ok {
|
|
||||||
var err error
|
|
||||||
a, err = c.AuthRefresh(a)
|
|
||||||
if err != nil {
|
|
||||||
// TODO: handle expired token error
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
var loginPassword string
|
|
||||||
if a == nil {
|
|
||||||
if pass, err := askPass("Password"); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
} else {
|
|
||||||
loginPassword = string(pass)
|
|
||||||
}
|
|
||||||
|
|
||||||
authInfo, err := c.AuthInfo(username)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
a, err = c.Auth(username, loginPassword, authInfo)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.TwoFactor.Enabled != 0 {
|
|
||||||
if a.TwoFactor.TOTP != 1 {
|
|
||||||
log.Fatal("Only TOTP is supported as a 2FA method")
|
|
||||||
}
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
|
||||||
fmt.Printf("2FA TOTP code: ")
|
|
||||||
scanner.Scan()
|
|
||||||
code := scanner.Text()
|
|
||||||
|
|
||||||
scope, err := c.AuthTOTP(code)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
a.Scope = scope
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var mailboxPassword string
|
|
||||||
if a.PasswordMode == protonmail.PasswordSingle {
|
|
||||||
mailboxPassword = loginPassword
|
|
||||||
}
|
|
||||||
if mailboxPassword == "" {
|
|
||||||
prompt := "Password"
|
|
||||||
if a.PasswordMode == protonmail.PasswordTwo {
|
|
||||||
prompt = "Mailbox password"
|
|
||||||
}
|
|
||||||
if pass, err := askPass(prompt); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
} else {
|
|
||||||
mailboxPassword = string(pass)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
keySalts, err := c.ListKeySalts()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = c.Unlock(a, keySalts, mailboxPassword)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
secretKey, bridgePassword, err := auth.GeneratePassword()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = auth.EncryptAndSave(&auth.CachedAuth{
|
|
||||||
Auth: *a,
|
|
||||||
LoginPassword: loginPassword,
|
|
||||||
MailboxPassword: mailboxPassword,
|
|
||||||
KeySalts: keySalts,
|
|
||||||
}, username, secretKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Bridge password:", bridgePassword)
|
|
||||||
case "status":
|
|
||||||
usernames, err := auth.ListUsernames()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(usernames) == 0 {
|
|
||||||
fmt.Printf("No logged in user.\n")
|
|
||||||
} else {
|
|
||||||
fmt.Printf("%v logged in user(s):\n", len(usernames))
|
|
||||||
for _, u := range usernames {
|
|
||||||
fmt.Printf("- %v\n", u)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "export-secret-keys":
|
|
||||||
exportSecretKeysCmd.Parse(flag.Args()[1:])
|
|
||||||
username := exportSecretKeysCmd.Arg(0)
|
|
||||||
if username == "" {
|
|
||||||
log.Fatal("usage: hydroxide export-secret-keys <username>")
|
|
||||||
}
|
|
||||||
|
|
||||||
bridgePassword, err := askBridgePass()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, privateKeys, err := auth.NewManager(newClient).Auth(username, bridgePassword)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wc, err := armor.Encode(os.Stdout, openpgp.PrivateKeyType, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, key := range privateKeys {
|
|
||||||
if err := key.SerializePrivate(wc, nil); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := wc.Close(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
case "import-messages":
|
|
||||||
importMessagesCmd.Parse(flag.Args()[1:])
|
|
||||||
username := importMessagesCmd.Arg(0)
|
|
||||||
archivePath := importMessagesCmd.Arg(1)
|
|
||||||
if username == "" {
|
|
||||||
log.Fatal("usage: hydroxide import-messages <username> [file]")
|
|
||||||
}
|
|
||||||
|
|
||||||
f := os.Stdin
|
|
||||||
if archivePath != "" {
|
|
||||||
f, err = os.Open(archivePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
bridgePassword, err := askBridgePass()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c, _, err := auth.NewManager(newClient).Auth(username, bridgePassword)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
br := bufio.NewReader(f)
|
|
||||||
if ok, err := isMbox(br); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
} else if ok {
|
|
||||||
mr := mbox.NewReader(br)
|
|
||||||
for {
|
|
||||||
r, err := mr.NextMessage()
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
} else if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := imports.ImportMessage(c, r); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := imports.ImportMessage(c, br); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "export-messages":
|
|
||||||
// TODO: allow specifying multiple IDs
|
|
||||||
var convID, msgID string
|
|
||||||
exportMessagesCmd.StringVar(&convID, "conversation-id", "", "conversation ID")
|
|
||||||
exportMessagesCmd.StringVar(&msgID, "message-id", "", "message ID")
|
|
||||||
exportMessagesCmd.Parse(flag.Args()[1:])
|
|
||||||
username := exportMessagesCmd.Arg(0)
|
|
||||||
if (convID == "" && msgID == "") || username == "" {
|
|
||||||
log.Fatal("usage: hydroxide export-messages [-conversation-id <id>] [-message-id <id>] <username>")
|
|
||||||
}
|
|
||||||
|
|
||||||
bridgePassword, err := askBridgePass()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c, privateKeys, err := auth.NewManager(newClient).Auth(username, bridgePassword)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mboxWriter := mbox.NewWriter(os.Stdout)
|
|
||||||
|
|
||||||
if convID != "" {
|
|
||||||
if err := exports.ExportConversationMbox(c, privateKeys, mboxWriter, convID); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if msgID != "" {
|
|
||||||
if err := exports.ExportMessageMbox(c, privateKeys, mboxWriter, msgID); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := mboxWriter.Close(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
case "smtp":
|
|
||||||
addr := *smtpHost + ":" + *smtpPort
|
|
||||||
authManager := auth.NewManager(newClient)
|
|
||||||
log.Fatal(listenAndServeSMTP(addr, debug, authManager, tlsConfig))
|
|
||||||
case "imap":
|
|
||||||
addr := *imapHost + ":" + *imapPort
|
|
||||||
authManager := auth.NewManager(newClient)
|
|
||||||
eventsManager := events.NewManager()
|
|
||||||
log.Fatal(listenAndServeIMAP(addr, debug, authManager, eventsManager, tlsConfig))
|
|
||||||
case "carddav":
|
|
||||||
addr := *carddavHost + ":" + *carddavPort
|
|
||||||
authManager := auth.NewManager(newClient)
|
|
||||||
eventsManager := events.NewManager()
|
|
||||||
log.Fatal(listenAndServeCardDAV(addr, authManager, eventsManager, tlsConfig))
|
|
||||||
case "serve":
|
|
||||||
smtpAddr := *smtpHost + ":" + *smtpPort
|
|
||||||
imapAddr := *imapHost + ":" + *imapPort
|
|
||||||
carddavAddr := *carddavHost + ":" + *carddavPort
|
|
||||||
|
|
||||||
authManager := auth.NewManager(newClient)
|
|
||||||
eventsManager := events.NewManager()
|
|
||||||
|
|
||||||
done := make(chan error, 3)
|
|
||||||
if !*disableSMTP {
|
|
||||||
go func() {
|
|
||||||
done <- listenAndServeSMTP(smtpAddr, debug, authManager, tlsConfig)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
if !*disableIMAP {
|
|
||||||
go func() {
|
|
||||||
done <- listenAndServeIMAP(imapAddr, debug, authManager, eventsManager, tlsConfig)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
if !*disableCardDAV {
|
|
||||||
go func() {
|
|
||||||
done <- listenAndServeCardDAV(carddavAddr, authManager, eventsManager, tlsConfig)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
log.Fatal(<-done)
|
|
||||||
case "sendmail":
|
|
||||||
username := flag.Arg(1)
|
|
||||||
if username == "" || flag.Arg(2) != "--" {
|
|
||||||
log.Fatal("usage: hydroxide sendmail <username> -- <args...>")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: other sendmail flags
|
|
||||||
var dotEOF bool
|
|
||||||
sendmailCmd.BoolVar(&dotEOF, "i", false, "don't treat a line with only a . character as the end of input")
|
|
||||||
sendmailCmd.Parse(flag.Args()[3:])
|
|
||||||
rcpt := sendmailCmd.Args()
|
|
||||||
|
|
||||||
bridgePassword, err := askBridgePass()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c, privateKeys, err := auth.NewManager(newClient).Auth(username, bridgePassword)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := c.GetCurrentUser()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
addrs, err := c.ListAddresses()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = smtpbackend.SendMail(c, u, privateKeys, addrs, rcpt, os.Stdin)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
fmt.Print(usage)
|
|
||||||
if cmd != "help" {
|
|
||||||
log.Fatal("Unrecognized command")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,10 +5,10 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/emersion/hydroxide/protonmail"
|
"github.com/0ranki/hydroxide-push/protonmail"
|
||||||
)
|
)
|
||||||
|
|
||||||
const pollInterval = 30 * time.Second
|
const pollInterval = 10 * time.Second
|
||||||
|
|
||||||
type Receiver struct {
|
type Receiver struct {
|
||||||
c *protonmail.Client
|
c *protonmail.Client
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
"github.com/emersion/go-message/mail"
|
"github.com/emersion/go-message/mail"
|
||||||
"github.com/emersion/go-message/textproto"
|
"github.com/emersion/go-message/textproto"
|
||||||
|
|
||||||
"github.com/emersion/hydroxide/protonmail"
|
"github.com/0ranki/hydroxide-push/protonmail"
|
||||||
)
|
)
|
||||||
|
|
||||||
func writeMessage(c *protonmail.Client, privateKeys openpgp.KeyRing, w io.Writer, msg *protonmail.Message) error {
|
func writeMessage(c *protonmail.Client, privateKeys openpgp.KeyRing, w io.Writer, msg *protonmail.Message) error {
|
||||||
|
|
25
go.mod
25
go.mod
|
@ -1,19 +1,24 @@
|
||||||
module github.com/emersion/hydroxide
|
module github.com/0ranki/hydroxide-push
|
||||||
|
|
||||||
go 1.13
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c
|
github.com/ProtonMail/go-crypto v1.0.0
|
||||||
github.com/boltdb/bolt v1.3.1
|
github.com/boltdb/bolt v1.3.1
|
||||||
github.com/cloudflare/circl v1.3.6 // indirect
|
|
||||||
github.com/emersion/go-bcrypt v0.0.0-20170822072041-6e724a1baa63
|
github.com/emersion/go-bcrypt v0.0.0-20170822072041-6e724a1baa63
|
||||||
github.com/emersion/go-imap v1.2.1
|
github.com/emersion/go-imap v1.2.1
|
||||||
github.com/emersion/go-mbox v1.0.3
|
github.com/emersion/go-mbox v1.0.3
|
||||||
github.com/emersion/go-message v0.17.0
|
github.com/emersion/go-message v0.18.1
|
||||||
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
|
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43
|
||||||
github.com/emersion/go-smtp v0.19.0
|
github.com/emersion/go-smtp v0.21.1
|
||||||
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9
|
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9
|
||||||
github.com/emersion/go-webdav v0.3.2-0.20220524091811-5d845721d8f7
|
github.com/emersion/go-webdav v0.5.0
|
||||||
golang.org/x/crypto v0.15.0
|
golang.org/x/crypto v0.22.0
|
||||||
golang.org/x/term v0.14.0
|
golang.org/x/term v0.19.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/cloudflare/circl v1.3.7 // indirect
|
||||||
|
golang.org/x/sys v0.19.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
43
go.sum
43
go.sum
|
@ -1,40 +1,40 @@
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE=
|
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||||
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
||||||
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
||||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||||
github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg=
|
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||||
github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||||
github.com/emersion/go-bcrypt v0.0.0-20170822072041-6e724a1baa63 h1:7aCSuwTBzg7BCPRRaBJD0weKZYdeAykOrY6ktpx8Vvc=
|
github.com/emersion/go-bcrypt v0.0.0-20170822072041-6e724a1baa63 h1:7aCSuwTBzg7BCPRRaBJD0weKZYdeAykOrY6ktpx8Vvc=
|
||||||
github.com/emersion/go-bcrypt v0.0.0-20170822072041-6e724a1baa63/go.mod h1:eRwwJnuLVFtYTC+AI2JDJTMcuQUTYhBIK4I6bC5tpqw=
|
github.com/emersion/go-bcrypt v0.0.0-20170822072041-6e724a1baa63/go.mod h1:eRwwJnuLVFtYTC+AI2JDJTMcuQUTYhBIK4I6bC5tpqw=
|
||||||
github.com/emersion/go-ical v0.0.0-20200224201310-cd514449c39e/go.mod h1:4xVTBPcT43a1pp3vdaa+FuRdX5XhKCZPpWv7m0z9ByM=
|
github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f/go.mod h1:2MKFUgfNMULRxqZkadG1Vh44we3y5gJAtTBlVsx1BKQ=
|
||||||
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
|
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
|
||||||
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
|
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
|
||||||
github.com/emersion/go-mbox v1.0.3 h1:Kac75r/EGi6KZAz48HXal9q7EiaXNl+U5HZfyDz0LKM=
|
github.com/emersion/go-mbox v1.0.3 h1:Kac75r/EGi6KZAz48HXal9q7EiaXNl+U5HZfyDz0LKM=
|
||||||
github.com/emersion/go-mbox v1.0.3/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI=
|
github.com/emersion/go-mbox v1.0.3/go.mod h1:Yp9IVuuOYLEuMv4yjgDHvhb5mHOcYH6x92Oas3QqEZI=
|
||||||
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
|
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
|
||||||
github.com/emersion/go-message v0.17.0 h1:NIdSKHiVUx4qKqdd0HyJFD41cW8iFguM2XJnRZWQH04=
|
github.com/emersion/go-message v0.18.1 h1:tfTxIoXFSFRwWaZsgnqS1DSZuGpYGzSmCZD8SK3QA2E=
|
||||||
github.com/emersion/go-message v0.17.0/go.mod h1:/9Bazlb1jwUNB0npYYBsdJ2EMOiiyN3m5UVHbY7GoNw=
|
github.com/emersion/go-message v0.18.1/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=
|
||||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||||
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY=
|
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY=
|
||||||
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||||
github.com/emersion/go-smtp v0.19.0 h1:iVCDtR2/JY3RpKoaZ7u6I/sb52S3EzfNHO1fAWVHgng=
|
github.com/emersion/go-smtp v0.21.1 h1:VQeZSZAKk8ueYii1yR5Zalmy7jI287eWDUqSaJ68vRM=
|
||||||
github.com/emersion/go-smtp v0.19.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
github.com/emersion/go-smtp v0.21.1/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
|
|
||||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||||
github.com/emersion/go-vcard v0.0.0-20191221110513-5f81fa0d3cc7/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
|
|
||||||
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 h1:ATgqloALX6cHCranzkLb8/zjivwQ9DWWDCQRnxTPfaA=
|
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 h1:ATgqloALX6cHCranzkLb8/zjivwQ9DWWDCQRnxTPfaA=
|
||||||
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
|
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
|
||||||
github.com/emersion/go-webdav v0.3.2-0.20220524091811-5d845721d8f7 h1:HqrKOBl8HdSnlo8kz72tCU36aK3WwSmpnnz04+dD0oc=
|
github.com/emersion/go-webdav v0.5.0 h1:Ak/BQLgAihJt/UxJbCsEXDPxS5Uw4nZzgIMOq3rkKjc=
|
||||||
github.com/emersion/go-webdav v0.3.2-0.20220524091811-5d845721d8f7/go.mod h1:uSM1VveeKtogBVWaYccTksToczooJ0rrVGNsgnDsr4Q=
|
github.com/emersion/go-webdav v0.5.0/go.mod h1:ycyIzTelG5pHln4t+Y32/zBvmrM7+mV7x+V+Gx4ZQno=
|
||||||
|
github.com/teambition/rrule-go v1.7.2/go.mod h1:mBJ1Ht5uboJ6jexKdNUJg2NcwP8uUMNvStWXlJD3MvU=
|
||||||
|
github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
|
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||||
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
|
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
@ -43,7 +43,6 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
@ -56,17 +55,15 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||||
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
|
||||||
golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8=
|
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||||
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
@ -74,8 +71,6 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|
||||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
## Remove this ConfigMap section after the initial run
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: hydroxide-push-config
|
||||||
|
data:
|
||||||
|
PROTON_ACCT: "my.account@protonmail.com"
|
||||||
|
PROTON_ACCT_PASSWORD: "myprotonaccountpassword"
|
||||||
|
PUSH_URL: "http://ntfy.sh"
|
||||||
|
PUSH_TOPIC: ""
|
||||||
|
## Remove the above after first run
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
creationTimestamp: "2024-03-27T07:02:59Z"
|
||||||
|
labels:
|
||||||
|
app: hydroxide-push
|
||||||
|
name: hydroxide-push
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- args:
|
||||||
|
- notify
|
||||||
|
image: ghcr.io/0ranki/hydroxide-push:latest
|
||||||
|
name: main
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /data
|
||||||
|
name: hydroxide-push-pvc
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: hydroxide-push-config
|
||||||
|
optional: true
|
||||||
|
volumes:
|
||||||
|
- name: hydroxide-push-pvc
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: hydroxide-push
|
||||||
|
|
|
@ -7,8 +7,8 @@ import (
|
||||||
"github.com/emersion/go-imap"
|
"github.com/emersion/go-imap"
|
||||||
imapbackend "github.com/emersion/go-imap/backend"
|
imapbackend "github.com/emersion/go-imap/backend"
|
||||||
|
|
||||||
"github.com/emersion/hydroxide/auth"
|
"github.com/0ranki/hydroxide-push/auth"
|
||||||
"github.com/emersion/hydroxide/events"
|
"github.com/0ranki/hydroxide-push/events"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errNotYetImplemented = errors.New("not yet implemented")
|
var errNotYetImplemented = errors.New("not yet implemented")
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/boltdb/bolt"
|
"github.com/boltdb/bolt"
|
||||||
|
|
||||||
"github.com/emersion/hydroxide/protonmail"
|
"github.com/0ranki/hydroxide-push/protonmail"
|
||||||
)
|
)
|
||||||
|
|
||||||
func serializeUID(uid uint32) []byte {
|
func serializeUID(uid uint32) []byte {
|
||||||
|
|
|
@ -6,8 +6,8 @@ import (
|
||||||
|
|
||||||
"github.com/boltdb/bolt"
|
"github.com/boltdb/bolt"
|
||||||
|
|
||||||
"github.com/emersion/hydroxide/config"
|
"github.com/0ranki/hydroxide-push/config"
|
||||||
"github.com/emersion/hydroxide/protonmail"
|
"github.com/0ranki/hydroxide-push/protonmail"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrNotFound = errors.New("message not found in local database")
|
var ErrNotFound = errors.New("message not found in local database")
|
||||||
|
|
|
@ -10,8 +10,8 @@ import (
|
||||||
"github.com/emersion/go-imap"
|
"github.com/emersion/go-imap"
|
||||||
imapbackend "github.com/emersion/go-imap/backend"
|
imapbackend "github.com/emersion/go-imap/backend"
|
||||||
|
|
||||||
"github.com/emersion/hydroxide/imap/database"
|
"github.com/0ranki/hydroxide-push/imap/database"
|
||||||
"github.com/emersion/hydroxide/protonmail"
|
"github.com/0ranki/hydroxide-push/protonmail"
|
||||||
)
|
)
|
||||||
|
|
||||||
const delimiter = "/"
|
const delimiter = "/"
|
||||||
|
|
|
@ -15,7 +15,7 @@ import (
|
||||||
"github.com/emersion/go-message"
|
"github.com/emersion/go-message"
|
||||||
"github.com/emersion/go-message/mail"
|
"github.com/emersion/go-message/mail"
|
||||||
|
|
||||||
"github.com/emersion/hydroxide/protonmail"
|
"github.com/0ranki/hydroxide-push/protonmail"
|
||||||
)
|
)
|
||||||
|
|
||||||
func messageID(msg *protonmail.Message) string {
|
func messageID(msg *protonmail.Message) string {
|
||||||
|
|
141
imap/user.go
141
imap/user.go
|
@ -1,16 +1,17 @@
|
||||||
package imap
|
package imap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/0ranki/hydroxide-push/ntfy"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/0ranki/hydroxide-push/events"
|
||||||
|
"github.com/0ranki/hydroxide-push/imap/database"
|
||||||
|
"github.com/0ranki/hydroxide-push/protonmail"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp"
|
"github.com/ProtonMail/go-crypto/openpgp"
|
||||||
"github.com/emersion/go-imap"
|
"github.com/emersion/go-imap"
|
||||||
imapbackend "github.com/emersion/go-imap/backend"
|
imapbackend "github.com/emersion/go-imap/backend"
|
||||||
"github.com/emersion/hydroxide/events"
|
|
||||||
"github.com/emersion/hydroxide/imap/database"
|
|
||||||
"github.com/emersion/hydroxide/protonmail"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var systemMailboxes = []struct {
|
var systemMailboxes = []struct {
|
||||||
|
@ -117,7 +118,7 @@ func newUser(be *backend, username string, c *protonmail.Client, privateKeys ope
|
||||||
go uu.receiveEvents(be.updates, ch)
|
go uu.receiveEvents(be.updates, ch)
|
||||||
uu.eventsReceiver = be.eventsManager.Register(c, u.Name, ch, done)
|
uu.eventsReceiver = be.eventsManager.Register(c, u.Name, ch, done)
|
||||||
|
|
||||||
log.Printf("User %q logged in via IMAP", u.Name)
|
log.Printf("Logged in as user %q", u.Name)
|
||||||
return uu, nil
|
return uu, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -347,75 +348,77 @@ func (u *user) receiveEvents(updates chan<- imapbackend.Update, events <-chan *p
|
||||||
eventUpdates = append(eventUpdates, update)
|
eventUpdates = append(eventUpdates, update)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Send push notification to topic
|
||||||
|
go ntfy.Notify()
|
||||||
case protonmail.EventUpdate, protonmail.EventUpdateFlags:
|
case protonmail.EventUpdate, protonmail.EventUpdateFlags:
|
||||||
log.Println("Received update event for message", eventMessage.ID)
|
log.Println("Received update event for message", eventMessage.ID)
|
||||||
createdSeqNums, deletedSeqNums, err := u.db.UpdateMessage(eventMessage.ID, eventMessage.Updated)
|
// createdSeqNums, deletedSeqNums, err := u.db.UpdateMessage(eventMessage.ID, eventMessage.Updated)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
log.Printf("cannot handle update event for message %s: cannot update message in local DB: %v", eventMessage.ID, err)
|
// log.Printf("cannot handle update event for message %s: cannot update message in local DB: %v", eventMessage.ID, err)
|
||||||
break
|
// break
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
for labelID, seqNum := range createdSeqNums {
|
// for labelID, seqNum := range createdSeqNums {
|
||||||
if mbox := u.getMailboxByLabel(labelID); mbox != nil {
|
// if mbox := u.getMailboxByLabel(labelID); mbox != nil {
|
||||||
update := new(imapbackend.MailboxUpdate)
|
// update := new(imapbackend.MailboxUpdate)
|
||||||
update.Update = imapbackend.NewUpdate(u.u.Name, mbox.name)
|
// update.Update = imapbackend.NewUpdate(u.u.Name, mbox.name)
|
||||||
update.MailboxStatus = imap.NewMailboxStatus(mbox.name, []imap.StatusItem{imap.StatusMessages})
|
// update.MailboxStatus = imap.NewMailboxStatus(mbox.name, []imap.StatusItem{imap.StatusMessages})
|
||||||
update.MailboxStatus.Messages = seqNum
|
// update.MailboxStatus.Messages = seqNum
|
||||||
eventUpdates = append(eventUpdates, update)
|
// eventUpdates = append(eventUpdates, update)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
for labelID, seqNum := range deletedSeqNums {
|
// for labelID, seqNum := range deletedSeqNums {
|
||||||
if mbox := u.getMailboxByLabel(labelID); mbox != nil {
|
// if mbox := u.getMailboxByLabel(labelID); mbox != nil {
|
||||||
update := new(imapbackend.ExpungeUpdate)
|
// update := new(imapbackend.ExpungeUpdate)
|
||||||
update.Update = imapbackend.NewUpdate(u.u.Name, mbox.name)
|
// update.Update = imapbackend.NewUpdate(u.u.Name, mbox.name)
|
||||||
update.SeqNum = seqNum
|
// update.SeqNum = seqNum
|
||||||
eventUpdates = append(eventUpdates, update)
|
// eventUpdates = append(eventUpdates, update)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// Send message updates
|
// // Send message updates
|
||||||
msg, err := u.db.Message(eventMessage.ID)
|
// msg, err := u.db.Message(eventMessage.ID)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
log.Printf("cannot handle update event for message %s: cannot get updated message from local DB: %v", eventMessage.ID, err)
|
// log.Printf("cannot handle update event for message %s: cannot get updated message from local DB: %v", eventMessage.ID, err)
|
||||||
break
|
// break
|
||||||
}
|
// }
|
||||||
for _, labelID := range msg.LabelIDs {
|
// for _, labelID := range msg.LabelIDs {
|
||||||
if _, created := createdSeqNums[labelID]; created {
|
// if _, created := createdSeqNums[labelID]; created {
|
||||||
// This message has been added to the label's mailbox
|
// // This message has been added to the label's mailbox
|
||||||
// No need to send a message update
|
// // No need to send a message update
|
||||||
continue
|
// continue
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if mbox := u.getMailboxByLabel(labelID); mbox != nil {
|
// if mbox := u.getMailboxByLabel(labelID); mbox != nil {
|
||||||
seqNum, _, err := mbox.db.FromApiID(eventMessage.ID)
|
// seqNum, _, err := mbox.db.FromApiID(eventMessage.ID)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
log.Printf("cannot handle update event for message %s: cannot get message sequence number in %s: %v", eventMessage.ID, mbox.name, err)
|
// log.Printf("cannot handle update event for message %s: cannot get message sequence number in %s: %v", eventMessage.ID, mbox.name, err)
|
||||||
continue
|
// continue
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
update := new(imapbackend.MessageUpdate)
|
// update := new(imapbackend.MessageUpdate)
|
||||||
update.Update = imapbackend.NewUpdate(u.u.Name, mbox.name)
|
// update.Update = imapbackend.NewUpdate(u.u.Name, mbox.name)
|
||||||
update.Message = imap.NewMessage(seqNum, []imap.FetchItem{imap.FetchFlags})
|
// update.Message = imap.NewMessage(seqNum, []imap.FetchItem{imap.FetchFlags})
|
||||||
update.Message.Flags = mbox.fetchFlags(msg)
|
// update.Message.Flags = mbox.fetchFlags(msg)
|
||||||
eventUpdates = append(eventUpdates, update)
|
// eventUpdates = append(eventUpdates, update)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
case protonmail.EventDelete:
|
case protonmail.EventDelete:
|
||||||
log.Println("Received delete event for message", eventMessage.ID)
|
log.Println("Received delete event for message", eventMessage.ID)
|
||||||
seqNums, err := u.db.DeleteMessage(eventMessage.ID)
|
// seqNums, err := u.db.DeleteMessage(eventMessage.ID)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
log.Printf("cannot handle delete event for message %s: cannot delete message from local DB: %v", eventMessage.ID, err)
|
// log.Printf("cannot handle delete event for message %s: cannot delete message from local DB: %v", eventMessage.ID, err)
|
||||||
break
|
// break
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
for labelID, seqNum := range seqNums {
|
// for labelID, seqNum := range seqNums {
|
||||||
if mbox := u.getMailboxByLabel(labelID); mbox != nil {
|
// if mbox := u.getMailboxByLabel(labelID); mbox != nil {
|
||||||
update := new(imapbackend.ExpungeUpdate)
|
// update := new(imapbackend.ExpungeUpdate)
|
||||||
update.Update = imapbackend.NewUpdate(u.u.Name, mbox.name)
|
// update.Update = imapbackend.NewUpdate(u.u.Name, mbox.name)
|
||||||
update.SeqNum = seqNum
|
// update.SeqNum = seqNum
|
||||||
eventUpdates = append(eventUpdates, update)
|
// eventUpdates = append(eventUpdates, update)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||||
"github.com/emersion/go-message/mail"
|
"github.com/emersion/go-message/mail"
|
||||||
|
|
||||||
"github.com/emersion/hydroxide/protonmail"
|
"github.com/0ranki/hydroxide-push/protonmail"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ImportMessage(c *protonmail.Client, r io.Reader) error {
|
func ImportMessage(c *protonmail.Client, r io.Reader) error {
|
||||||
|
|
|
@ -0,0 +1,257 @@
|
||||||
|
package ntfy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"golang.org/x/crypto/ssh/terminal"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/0ranki/hydroxide-push/auth"
|
||||||
|
"github.com/0ranki/hydroxide-push/config"
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-imap/backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NtfyConfig struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
Topic string `json:"topic"`
|
||||||
|
BridgePw string `json:"bridgePw"`
|
||||||
|
User string `json:"user"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *NtfyConfig) Init() {
|
||||||
|
if cfg.Topic == "" {
|
||||||
|
r := make([]byte, 12)
|
||||||
|
_, err := rand.Read(r)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
cfg.Topic = strings.Replace(base64.StdEncoding.EncodeToString(r), "/", "+", -1)
|
||||||
|
|
||||||
|
}
|
||||||
|
if cfg.URL == "" {
|
||||||
|
cfg.URL = "http://ntfy.sh"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *NtfyConfig) URI() string {
|
||||||
|
return fmt.Sprintf("%s/%s", cfg.URL, cfg.Topic)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *NtfyConfig) Save() error {
|
||||||
|
b, err := json.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
path, err := ntfyConfigFile()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(path, b, 0600)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ntfyConfigFile() (string, error) {
|
||||||
|
return config.Path("notify.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Notify() {
|
||||||
|
cfg := NtfyConfig{}
|
||||||
|
if err := cfg.Read(); err != nil {
|
||||||
|
log.Printf("error reading configuration: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req, _ := http.NewRequest("POST", cfg.URI(), strings.NewReader("New message received"))
|
||||||
|
if cfg.User != "" && cfg.Password != "" {
|
||||||
|
pw, err := base64.StdEncoding.DecodeString(cfg.Password)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error decoding push endpoint password: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.SetBasicAuth(cfg.User, string(pw))
|
||||||
|
}
|
||||||
|
req.Header.Set("Title", "ProtonMail")
|
||||||
|
req.Header.Set("Click", "dismiss")
|
||||||
|
req.Header.Set("Tags", "envelope")
|
||||||
|
if _, err := http.DefaultClient.Do(req); err != nil {
|
||||||
|
log.Printf("failed to publish to push topic: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("Push event sent")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read reads the configuration from file. Creates the file
|
||||||
|
// if it does not exist
|
||||||
|
func (cfg *NtfyConfig) Read() error {
|
||||||
|
f, err := ntfyConfigFile()
|
||||||
|
if err == nil {
|
||||||
|
b, err := os.ReadFile(f)
|
||||||
|
if err == nil {
|
||||||
|
err = json.Unmarshal(b, &cfg)
|
||||||
|
} else if strings.HasSuffix(err.Error(), "no such file or directory") {
|
||||||
|
cfg.Init()
|
||||||
|
err = cfg.Save()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
cfg.Init()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoginBridge(cfg *NtfyConfig) error {
|
||||||
|
if cfg.BridgePw == "" {
|
||||||
|
cfg.BridgePw = os.Getenv("HYDROXIDE_BRIDGE_PASSWORD")
|
||||||
|
}
|
||||||
|
if cfg.BridgePw == "" {
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
fmt.Printf("Bridge password: ")
|
||||||
|
scanner.Scan()
|
||||||
|
cfg.BridgePw = scanner.Text()
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func Login(cfg *NtfyConfig, be backend.Backend) {
|
||||||
|
//time.Sleep(1 * time.Second)
|
||||||
|
c, _ := net.ResolveIPAddr("ip", "127.0.0.1")
|
||||||
|
conn := imap.ConnInfo{
|
||||||
|
RemoteAddr: c,
|
||||||
|
LocalAddr: c,
|
||||||
|
TLS: nil,
|
||||||
|
}
|
||||||
|
usernames, err := auth.ListUsernames()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(usernames) > 1 {
|
||||||
|
log.Fatal("only one login supported for now")
|
||||||
|
}
|
||||||
|
err = cfg.Read()
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
if len(usernames) == 0 || cfg.URL == "" || cfg.Topic == "" {
|
||||||
|
executable, _ := os.Executable()
|
||||||
|
log.Println("login first using " + executable + " auth <protonmail username>")
|
||||||
|
log.Fatalln("then setup ntfy using " + executable + "setup-ntfy")
|
||||||
|
}
|
||||||
|
if cfg.BridgePw == "" {
|
||||||
|
err = LoginBridge(cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = be.Login(&conn, usernames[0], cfg.BridgePw)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *NtfyConfig) Setup() {
|
||||||
|
|
||||||
|
// Configure using environment
|
||||||
|
if os.Getenv("PUSH_URL") != "" && os.Getenv("PUSH_TOPIC") != "" {
|
||||||
|
cfg.URL = os.Getenv("PUSH_URL")
|
||||||
|
cfg.Topic = os.Getenv("PUSH_TOPIC")
|
||||||
|
log.Printf("Current push endpoint: %s\n", cfg.URI())
|
||||||
|
if os.Getenv("PUSH_USER") != "" && os.Getenv("PUSH_PASSWORD") != "" {
|
||||||
|
cfg.User = os.Getenv("PUSH_USER")
|
||||||
|
cfg.Password = base64.StdEncoding.EncodeToString([]byte(os.Getenv("PUSH_PASSWORD")))
|
||||||
|
log.Println("Authentication for push endpoint configured using environment")
|
||||||
|
} else {
|
||||||
|
log.Println("Both PUSH_USER and PUSH_PASSWORD not set, assuming no authentication is necessary.")
|
||||||
|
}
|
||||||
|
err := cfg.Save()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var n string
|
||||||
|
if cfg.URL != "" && cfg.Topic != "" {
|
||||||
|
fmt.Printf("Current push endpoint: %s\n", cfg.URI())
|
||||||
|
n = "new "
|
||||||
|
}
|
||||||
|
if cfg.User != "" && cfg.Password != "" {
|
||||||
|
fmt.Println("Push is currently configured for basic auth. You'll need to input credentials again")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read push base URL
|
||||||
|
notValid := true
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
for notValid {
|
||||||
|
tmpURL := cfg.URL
|
||||||
|
fmt.Printf("Input %spush server URL ('%s') : ", n, cfg.URL)
|
||||||
|
scanner.Scan()
|
||||||
|
if len(scanner.Text()) > 0 {
|
||||||
|
tmpURL = scanner.Text()
|
||||||
|
}
|
||||||
|
if _, err := url.ParseRequestURI(tmpURL); err != nil {
|
||||||
|
fmt.Printf("Not a valid URL: %s\n", tmpURL)
|
||||||
|
} else {
|
||||||
|
notValid = false
|
||||||
|
cfg.URL = tmpURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scanner = bufio.NewScanner(os.Stdin)
|
||||||
|
// Read push topic
|
||||||
|
fmt.Printf("Input push topic ('%s'): ", cfg.Topic)
|
||||||
|
scanner.Scan()
|
||||||
|
if len(scanner.Text()) > 0 {
|
||||||
|
cfg.Topic = scanner.Text()
|
||||||
|
}
|
||||||
|
fmt.Printf("Using URL %s\n", cfg.URI())
|
||||||
|
// Configure HTTP Basic Auth for push
|
||||||
|
// This needs to be input each time the auth flow is done,
|
||||||
|
// existing values are reset
|
||||||
|
cfg.User = ""
|
||||||
|
cfg.Password = ""
|
||||||
|
fmt.Println("Configuring HTTP basic authentication for push endpoint.")
|
||||||
|
fmt.Println("Previously set username and password have been cleared.")
|
||||||
|
fmt.Println("Leave values blank to disable basic authentication.")
|
||||||
|
scanner = bufio.NewScanner(os.Stdin)
|
||||||
|
fmt.Printf("Username: ")
|
||||||
|
scanner.Scan()
|
||||||
|
if len(scanner.Text()) > 0 {
|
||||||
|
cfg.User = scanner.Text()
|
||||||
|
}
|
||||||
|
fmt.Printf("Password: ")
|
||||||
|
pwBytes, err := terminal.ReadPassword(0)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error reading password: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(pwBytes) > 0 {
|
||||||
|
// Store the password in base64 for a little obfuscation
|
||||||
|
cfg.Password = base64.StdEncoding.EncodeToString(pwBytes)
|
||||||
|
}
|
||||||
|
// Save bridge password
|
||||||
|
if len(cfg.BridgePw) == 0 {
|
||||||
|
err := LoginBridge(cfg)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("Bridge password is set")
|
||||||
|
}
|
||||||
|
// Save configuration
|
||||||
|
err = cfg.Save()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println("Notification configuration saved")
|
||||||
|
}
|
25
smtp/smtp.go
25
smtp/smtp.go
|
@ -11,10 +11,11 @@ import (
|
||||||
"github.com/ProtonMail/go-crypto/openpgp"
|
"github.com/ProtonMail/go-crypto/openpgp"
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||||
"github.com/emersion/go-message/mail"
|
"github.com/emersion/go-message/mail"
|
||||||
|
"github.com/emersion/go-sasl"
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
|
|
||||||
"github.com/emersion/hydroxide/auth"
|
"github.com/0ranki/hydroxide-push/auth"
|
||||||
"github.com/emersion/hydroxide/protonmail"
|
"github.com/0ranki/hydroxide-push/protonmail"
|
||||||
)
|
)
|
||||||
|
|
||||||
func toPMAddressList(addresses []*mail.Address) []*protonmail.MessageAddress {
|
func toPMAddressList(addresses []*mail.Address) []*protonmail.MessageAddress {
|
||||||
|
@ -377,7 +378,16 @@ type session struct {
|
||||||
allReceivers []string
|
allReceivers []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *session) AuthPlain(username, password string) error {
|
var _ interface {
|
||||||
|
smtp.Session
|
||||||
|
smtp.AuthSession
|
||||||
|
} = (*session)(nil)
|
||||||
|
|
||||||
|
func (s *session) AuthMechanisms() []string {
|
||||||
|
return []string{sasl.Plain}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) authPlain(username, password string) error {
|
||||||
c, privateKeys, err := s.be.sessions.Auth(username, password)
|
c, privateKeys, err := s.be.sessions.Auth(username, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -403,6 +413,15 @@ func (s *session) AuthPlain(username, password string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *session) Auth(mech string) (sasl.Server, error) {
|
||||||
|
return sasl.NewPlainServer(func(identity, username, password string) error {
|
||||||
|
if identity != "" && identity != username {
|
||||||
|
return fmt.Errorf("invalid SASL PLAIN identity")
|
||||||
|
}
|
||||||
|
return s.authPlain(username, password)
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *session) Mail(from string, options *smtp.MailOptions) error {
|
func (s *session) Mail(from string, options *smtp.MailOptions) error {
|
||||||
if s.c == nil {
|
if s.c == nil {
|
||||||
return smtp.ErrAuthRequired
|
return smtp.ErrAuthRequired
|
||||||
|
|
Loading…
Reference in New Issue