Skip to content

Commit

Permalink
initial release
Browse files Browse the repository at this point in the history
  • Loading branch information
clisboa committed Oct 10, 2021
1 parent 60449bf commit d1ba216
Show file tree
Hide file tree
Showing 14 changed files with 3,625 additions and 0 deletions.
37 changes: 37 additions & 0 deletions .gitignore
@@ -0,0 +1,37 @@
# Compiled source #
###################
*.com
*.class
*.dll
*.exe
*.o
*.so

# Packages #
############
# it's better to unpack these files and commit the raw source
# git has its own built in compression methods
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip

# Logs and databases #
######################
*.log
*.sql
*.sqlite

# OS generated files #
######################
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
Binary file added KoboRoot.tgz
Binary file not shown.
75 changes: 75 additions & 0 deletions README.md
@@ -1,2 +1,77 @@
# KoboMail
Experimental email attachment downloader for Kobo devices (gmail only ATM)

## What is KoboMail?
It is a software that will read emails sent to user+kobo@gmail.com and download the attached files to the Kobo device.
It doesn't restrict the filetype so try to keep the emails you send as clean as possible ;)
The software takes advantage of a particularity in Gmail which is you can add flags to you email like the +kobo in the example above and still receive those emails on the user@gmail.com account. This makes it much easier for searching and listing emails specifically targetting your Kobo device.
Once the email is treated by program it will automatically flag it as seen so it's not processed twice (there's a flag in the config file that allows overriding for such time you want to redownload all files you've sent).

### Warning

This software is experimental, if you don't want to risk corrupting your files, database, etc of your Kobo device don't use this.
Use this software AT YOUR OWN RISK.

## Changelog
**0.1.0**
* Initial release

## Installing

Quick start for Kobo devices:

1. download the latest [KoboRoot.tgz](https://github.com/clisboa/KoboMail/releases/download/v0.1/KoboRoot.tgz)
1. connect your Kobo device to your computer with a USB cable
3. place the KoboRoot.tgz file in the .kobo directory of your Kobo device
4. disconnect the reader

When you disconnect the Kobo device, it will perform the instalation of the KoboRoot.tgz files onto the device.
Once the installation is finished you can verify that KoboRoot.tgz is now gone.
No you should head to the .add/kobomail folder and edit the kobomail_cfg.toml file


```
# currently only gmail is supported
# you need to activate IMAP for your gmail account
imap_host = "imap.gmail.com"
imap_port = "993"
# gmail account
imap_user = "user@gmail.com"
# gmail app password. you will need to generate a password specifically for KoboMail
# this can be done here: https://support.google.com/mail/answer/185833?hl=en-GB
imap_pwd = "password"
# with gmail you can send an email to user+kobo@gmail.com and the email will land on user@gmail.com account
# you can customize the flag used to detect the emails you want to process specifically for the Kobo device
email_flag = "kobo"
# flag to process all emails sent fo user+kobo@gmail.com or only the unread emails
email_unseen = "true"
# If you want to uninstall KoboMail just place an empty file called UNINSTALL next to this configuration file
# and next time KoboMail runs it will delete itself
```

If the configuration is not correct KoboMail might not be able to work correctly.
You will need to activate IMAP on your gmail account and generate an app password as described in the config file.
Once the configuration is correct everytime your device connects to a Wifi access point the KoboMail program will run and process any emails sent to user+kobo@gmail.com that are not open yet.
If any messages were processed after a few seconds Kobo will display the dialog to connect to a PC, you don't need to actually physically connect a USB cable you just need to click on the Connect button. This is part of a workarround to trigger Kobo to recognize the new ebooks it just received via email.
After clicking on the connect button you will see the common full screen dialog as if Kobo was connected to a PC and shortly after it will show the import content progress bar.

You can attach multiple files to a single email, every attachment will be processed. All attachments will be dumped into the folder KoboMailLibrary.

There's a log.txt file in the .add/kobomail folder that will allow to diagnose problems.

## Uninstalling

Just place a file called UNINSTALL in the .add/kobomail folder and everything will be wiped clean except the KoboMailLibrary~.

## Further information.
This project includes bits and pieces of many different projects and ideas discussed in the mobileread.com forums, namely:
- https://github.com/shermp/kobo-rclone
- https://github.com/fsantini/KoboCloud
- https://gitlab.com/anarcat/wallabako

3 changes: 3 additions & 0 deletions etc/udev/rules.d/97-kobomail.rules
@@ -0,0 +1,3 @@
KERNEL=="eth*", ACTION=="add", RUN+="/usr/local/kobomail/kobomail_launcher.sh"
KERNEL=="wlan*", ACTION=="add", RUN+="/usr/local/kobomail/kobomail_launcher.sh"
KERNEL=="lo", RUN+="/usr/local/kobomail/kobomail_config_setup.sh"
14 changes: 14 additions & 0 deletions go.mod
@@ -0,0 +1,14 @@
module github.com/clisboa/kobomail

go 1.17

require (
github.com/BurntSushi/toml v0.4.1 // indirect
github.com/emersion/go-imap v1.2.0 // indirect
github.com/emersion/go-message v0.15.0 // indirect
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
github.com/gwatts/rootcerts v0.0.0-20210901182307-6417658c2540 // indirect
github.com/leprosus/golang-log v1.0.11 // indirect
golang.org/x/text v0.3.7 // indirect
)
23 changes: 23 additions & 0 deletions go.sum
@@ -0,0 +1,23 @@
github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw=
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/emersion/go-imap v1.2.0 h1:lyUQ3+EVM21/qbWE/4Ya5UG9r5+usDxlg4yfp3TgHFA=
github.com/emersion/go-imap v1.2.0/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
github.com/emersion/go-message v0.15.0 h1:urgKGqt2JAc9NFJcgncQcohHdiYb803YTH9OQwHBHIY=
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac h1:tn/OQ2PmwQ0XFVgAHfjlLyqMewry25Rz7jWnVoh4Ggs=
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
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/gwatts/rootcerts v0.0.0-20210901182307-6417658c2540 h1:33heCgbF4SKPnsQoFF5xn9aEu7EY6gWDX/8pxyhYHfo=
github.com/gwatts/rootcerts v0.0.0-20210901182307-6417658c2540/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leprosus/golang-log v1.0.11 h1:sy5c0KMwESzuosD5DOyQ8b73F7M1zeFReL0O2ESJsRk=
github.com/leprosus/golang-log v1.0.11/go.mod h1:ekiK3/I7IQHUMor8vmXJmz64PXAoZfHYVoOAFh2nyQs=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
231 changes: 231 additions & 0 deletions kobomail.go
@@ -0,0 +1,231 @@
package main

import (
"crypto/tls"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"strings"
"time"

"github.com/BurntSushi/toml"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/client"
"github.com/emersion/go-message/mail"
)

//config struct
type KoboMailConfig struct {
IMAP_Host string `toml:"imap_host"`
IMAP_Port string `toml:"imap_port"`
IMAP_User string `toml:"imap_user"`
IMAP_Pwd string `toml:"imap_pwd"`
Email_Flag string `toml:"email_flag"`
Email_Unseen string `toml:"email_unseen"`
}

// chkErrFatal prints a message to the Kobo screen, then exits the program
func chkErrFatal(err error, usrMsg string, msgDuration int) {
if err != nil {
if usrMsg != "" {
// fbPrint(usrMsg)
time.Sleep(time.Duration(msgDuration) * time.Second)
}
log.Fatal(err)
}
}

// // logErrPrint is a convenience function for logging errors
// func logErrPrint(err error) {
// if err != nil {
// log.Print(err)
// }
// }

// nickelUSBplug simulates pugging in a USB cable
func nickelUSBplug() {
nickelHWstatusPipe := "/tmp/nickel-hardware-status"
nickelPipe, _ := os.OpenFile(nickelHWstatusPipe, os.O_RDWR, os.ModeNamedPipe)
nickelPipe.WriteString("usb plug add")
nickelPipe.Close()
}

// nickelUSBunplug simulates unplugging a USB cable
func nickelUSBunplug() {
nickelHWstatusPipe := "/tmp/nickel-hardware-status"
nickelPipe, _ := os.OpenFile(nickelHWstatusPipe, os.O_RDWR, os.ModeNamedPipe)
nickelPipe.WriteString("usb plug remove")
nickelPipe.Close()
}

func main() {
// If the file doesn't exist, create it or append to the
KM_Log_Path := ""
if _, err := os.Stat("/mnt/onboard/.add/kobomail/logs.txt"); err == nil {
KM_Log_Path = "/mnt/onboard/.add/kobomail/logs.txt"
} else if os.IsNotExist(err) {
KM_Log_Path = "logs.txt"
}
logFile, err := os.OpenFile(KM_Log_Path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
log.Fatal(err)
}
//let's output to both stoud and log file
mw := io.MultiWriter(os.Stdout, logFile)
log.SetOutput(mw)

KM_Library_Path := ""
if _, err := os.Stat("/mnt/onboard/KoboMailLibrary/"); err == nil {
KM_Library_Path = "/mnt/onboard/KoboMailLibrary/"
} else if os.IsNotExist(err) {
KM_Library_Path = "Library/"
}

// Read Config file
KM_Config_Path := ""
if _, err := os.Stat("/mnt/onboard/.add/kobomail/kobomail_cfg.toml"); err == nil {
KM_Config_Path = "/mnt/onboard/.add/kobomail/kobomail_cfg.toml"
} else if os.IsNotExist(err) {
KM_Config_Path = "kobomail_cfg.toml"
}

var KM_Config KoboMailConfig
if _, err := toml.DecodeFile(KM_Config_Path, &KM_Config); err != nil {
chkErrFatal(err, "Couldn't read config. Aborting!", 5)
}

host := KM_Config.IMAP_Host
port := KM_Config.IMAP_Port
user := KM_Config.IMAP_User
pass := KM_Config.IMAP_Pwd
tlsn := ""
if port == "" {
port = "993"
}

connStr := fmt.Sprintf("%s:%s", host, port)

tlsc := &tls.Config{}
if tlsn != "" {
tlsc.ServerName = tlsn
}

c, err := client.DialTLS(connStr, tlsc)
if err != nil {
log.Fatal(err)
}
log.Println("Connected")
defer c.Logout()

if err := c.Login(user, pass); err != nil {
log.Fatal(err)
}
log.Println("Authenticated")

mbox, err := c.Select("INBOX", false)
if err != nil {
log.Fatal(err)
}
log.Println("Inbox selected: ", mbox.Name)

criteria := imap.NewSearchCriteria()
if KM_Config.Email_Unseen == "true" {
criteria.WithoutFlags = []string{"\\Seen"}
}
KM_Config_To_Kobo := strings.Replace(KM_Config.IMAP_User, "@", "+"+KM_Config.Email_Flag+"@", 1)
criteria.Header.Add("TO", KM_Config_To_Kobo)

uids, err := c.Search(criteria)
if err != nil {
log.Println(err)
}
seqset := new(imap.SeqSet)
seqset.AddNum(uids...)
log.Printf("Search complete, found %d messages", len(uids))

section := &imap.BodySectionName{}
items := []imap.FetchItem{imap.FetchEnvelope, imap.FetchFlags, imap.FetchInternalDate, section.FetchItem()}
messages := make(chan *imap.Message)
done := make(chan error, 1)
go func() {
done <- c.Fetch(seqset, items, messages)
log.Println("Fetch complete")
}()

for msg := range messages {
if msg != nil {
log.Printf("got message with address %p\n", msg)

r := msg.GetBody(section)
if r == nil {
log.Fatal("Server didn't returned message body")
}
// Create a new mail reader
mr, err := mail.CreateReader(r)
if err != nil {
log.Fatal(err)
}

// Print some info about the message
header := mr.Header
if date, err := header.Date(); err == nil {
log.Println("Date:", date)
}
// if from, err := header.AddressList("From"); err == nil {
// log.Println("From:", from)
// }
// if to, err := header.AddressList("To"); err == nil {
// log.Println("To:", to)
// }
if subject, err := header.Subject(); err == nil {
log.Println("Subject:", subject)
}

// Process each message's part
for {
p, err := mr.NextPart()
if err == io.EOF {
break
} else if err != nil {
log.Fatal(err)
}

switch h := p.Header.(type) {
case *mail.AttachmentHeader:
// This is an attachment
filename, _ := h.Filename()
log.Println("Got attachment:", filename)
contenttype, _, _ := h.ContentType()
log.Println("of type: ", contenttype)
b, _ := ioutil.ReadAll(p.Body)
// write the whole body at once
err = ioutil.WriteFile(KM_Library_Path+filename, b, 0644)
if err != nil {
panic(err)
}

}
}
} else {
log.Println("no messages matched criteria")
}
}
// if err := <-done; err != nil {
// log.Fatal(err)
// }

//now that we finished loading all messages we'll simulate the USB cable connect
//but only if there were any messages processed, no need to bug the user if there was nothing new
if len(uids) > 0 {
log.Println("Simulating PLugging USB cable and wait 10s for the user to click on the connect button")
nickelUSBplug()

time.Sleep(10 * time.Second)

log.Println("Simulating unplugging USB cable")
nickelUSBunplug()
//after this Nickel will do the job to import the new files loaded into the KoboMailLibrary folder
}
}
5 changes: 5 additions & 0 deletions makeKoboRoot.sh
@@ -0,0 +1,5 @@
#!/bin/sh

#GOOS=linux GOARCH=arm go build -o kobomail
cp kobomail usr/local/kobomail/
tar -cvzf KoboRoot.tgz -C . etc usr
Binary file added usr/local/kobomail/kobomail
Binary file not shown.

0 comments on commit d1ba216

Please sign in to comment.