Initial commit.
This commit is contained in:
commit
f1103e1978
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.idea
|
||||||
|
.vscode
|
54
README.md
Normal file
54
README.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# Evolved
|
||||||
|
|
||||||
|
Standalone application for updating Evolution PIM badge with number of unread emails.
|
||||||
|
|
||||||
|
It has no GUI and the only purpose of it - to run as user daemon, check for Evolution files changes, get unread messages count from it's database and send dbus event with new count, so you'll get exact number of unread messages in all your mailboxes on task bar.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
Golang 1.21+ and C compiler present in system and found in `PATH`, as Evolution database is in sqlite3.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Just:
|
||||||
|
|
||||||
|
```text
|
||||||
|
go install go.dev.pztrn.name/evolved@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using
|
||||||
|
|
||||||
|
### Via systemd
|
||||||
|
|
||||||
|
Put this in `~/.config/systemd/user/evolved.service`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=Evolution badge count.
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/path/to/evolved
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
```
|
||||||
|
Replace `/path/to/evolved` for real path to binary.
|
||||||
|
|
||||||
|
After that do:
|
||||||
|
|
||||||
|
```text
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable evolved.service
|
||||||
|
systemctl --user start evolved.service
|
||||||
|
```
|
||||||
|
|
||||||
|
### Via i3/sway/etc. configuration file for autostart
|
||||||
|
|
||||||
|
For i3 and sway there is `exec` thing that launches commands on startup. Feel free to use this.
|
||||||
|
|
||||||
|
Look for equivalent thing for other WMs.
|
||||||
|
|
||||||
|
## Caveat about desktop file name
|
||||||
|
|
||||||
|
Evolved using `org.gnome.Evolution.desktop` as launcher file name by default. Your distribution might name it differently, check `/usr/share/applications` or `~/.local/share/applications` for actual file name and pass `-desktop-file` parameter to Evolved binary.
|
11
go.mod
Normal file
11
go.mod
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
module go.dev.pztrn.name/evolved
|
||||||
|
|
||||||
|
go 1.21.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
|
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||||
|
golang.org/x/sys v0.13.0 // indirect
|
||||||
|
)
|
13
go.sum
Normal file
13
go.sum
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||||
|
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||||
|
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
|
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||||
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
207
main.go
Normal file
207
main.go
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"os/user"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
cliFlagIsDebug = flag.Bool("debug", false, "Enable debug output.")
|
||||||
|
cliFlagDesktopFileName = flag.String("desktop-file", "org.gnome.Evolution.desktop", "Desktop file name from /usr/share/applications or ~/.local/share/applications which is used for launching Evolution.")
|
||||||
|
databasesPaths = make([]string, 0)
|
||||||
|
|
||||||
|
dbusSession *dbus.Conn
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// CTRL+C handler.
|
||||||
|
signalHandler := make(chan os.Signal, 1)
|
||||||
|
shutdownDone := make(chan bool, 1)
|
||||||
|
|
||||||
|
signal.Notify(signalHandler, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
|
slog.Info("Starting Evolved...")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
if *cliFlagIsDebug {
|
||||||
|
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})))
|
||||||
|
|
||||||
|
slog.Debug("Debug output enabled!")
|
||||||
|
}
|
||||||
|
|
||||||
|
dbusConn, err := dbus.ConnectSessionBus()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to connect to dbus session bus!", "error", err)
|
||||||
|
|
||||||
|
os.Exit(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbusSession = dbusConn
|
||||||
|
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to start filesystem changes watcher!", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := getEvolutionMailDatabasesPaths(); err != nil {
|
||||||
|
slog.Error("Failed to get Evolution mail databases paths!", "error", err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, databasePath := range databasesPaths {
|
||||||
|
slog.Info("Starting listening for filesystem changes in Evolution directory...", "path", databasePath)
|
||||||
|
|
||||||
|
_ = watcher.Add(databasePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
go watchFSNotifications(watcher)
|
||||||
|
|
||||||
|
unreadCount, err := getEvolutionUnreadMailsCount()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to get unread counts!", "error", err)
|
||||||
|
|
||||||
|
os.Exit(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
emitDBusSignal(unreadCount)
|
||||||
|
|
||||||
|
slog.Info("Evolved started.")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-signalHandler
|
||||||
|
|
||||||
|
slog.Info("Shutting down Evolved...")
|
||||||
|
|
||||||
|
if err := watcher.Close(); err != nil {
|
||||||
|
slog.Error("Failed to stop filesystem watcher!", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
emitDBusSignal(0)
|
||||||
|
|
||||||
|
shutdownDone <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-shutdownDone
|
||||||
|
|
||||||
|
slog.Info("Evolved stopped.")
|
||||||
|
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func emitDBusSignal(unreadCount uint) {
|
||||||
|
params := make(map[string]dbus.Variant)
|
||||||
|
params["count"] = dbus.MakeVariant(unreadCount)
|
||||||
|
params["count-visible"] = dbus.MakeVariant(unreadCount > 0)
|
||||||
|
|
||||||
|
if err := dbusSession.Emit(
|
||||||
|
"/",
|
||||||
|
"com.canonical.Unity.LauncherEntry.Update",
|
||||||
|
"application://"+*cliFlagDesktopFileName,
|
||||||
|
params,
|
||||||
|
); err != nil {
|
||||||
|
slog.Error("Failed to emit badge data via dbus!", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEvolutionMailDatabasesPaths() error {
|
||||||
|
userData, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getEvolutionMailDatabasesPaths: get current user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := filepath.Walk(path.Join(userData.HomeDir, ".cache", "evolution", "mail"), func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err == nil && strings.HasSuffix(info.Name(), "folders.db") {
|
||||||
|
databasesPaths = append(databasesPaths, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("getEvolutionMailDatabasesPaths: get databases paths: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEvolutionUnreadMailsCount() (uint, error) {
|
||||||
|
unreadCount := uint(0)
|
||||||
|
|
||||||
|
ctx, _ := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
|
||||||
|
for _, dbFile := range databasesPaths {
|
||||||
|
db, err := sqlx.Connect("sqlite3", dbFile)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to open Evolution database file!", "file", dbFile, "error", err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
counts := make([]uint, 0)
|
||||||
|
|
||||||
|
if err := db.SelectContext(ctx, &counts, "SELECT unread_count FROM folders"); err != nil {
|
||||||
|
slog.Error("Failed to get unread counts!", "file", dbFile, "error", err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, count := range counts {
|
||||||
|
unreadCount += count
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Close(); err != nil {
|
||||||
|
slog.Error("Failed to close database file for reading! Expect unexpected!", "file", dbFile, "error", err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return unreadCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func watchFSNotifications(watcher *fsnotify.Watcher) {
|
||||||
|
slog.Debug("Starting filesystem watcher goroutine...")
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-watcher.Events:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("Got filesystem event", "event", event.Name, "op", event.Op.String())
|
||||||
|
|
||||||
|
unreadCount, err := getEvolutionUnreadMailsCount()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to get unread count in watcher goroutine!", "error", err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Got unread count", "count", unreadCount)
|
||||||
|
|
||||||
|
emitDBusSignal(unreadCount)
|
||||||
|
|
||||||
|
case err, ok := <-watcher.Errors:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Error("Got error from filesystem watcher!", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user