Initial commit.

This commit is contained in:
Stanislav Nikitin 2023-11-03 19:54:04 +05:00
commit f1103e1978
Signed by: pztrn
GPG Key ID: 1E944A0F0568B550
5 changed files with 287 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.idea
.vscode

54
README.md Normal file
View 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
View 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
View 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
View 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)
}
}
}