Nathan Grigg

Scan2email

I have a 5-year-old Brother Scanner (the ADS-1700W) that serves me pretty well. It is small enough to keep on my desk, fast, and has a Wi-Fi connection. But getting scans from the scanner to the computer is sometimes bothersome. My primary way of using it is to scan to a network folder on my home server, which is great for archiving things, but not so great for family members or when I need something right away.

My preferred way to get a quick scan is by email. You immediately have it on whatever device you are using, and you have it saved for later if you need it. The Brother Scanner has a scan-to-email function, but it is buggy. Specifically, it sends slightly malformed emails that Fastmail accepts but Gmail returns to sender.

But since network scanning is rock solid, last year I wrote a program to watch a set of folders and send by email whatever files it finds. I was on a bit of a Go kick at the time, and I think Go works pretty well for the task.

Here is the relatively short program that I wrote. It is meant to be running as a daemon, and as long as it is running, it will email you all the files that it finds. Since the interesting parts are at the end and I don’t think anyone will read that far, I’ll show you the program in reverse order, starting with the main function.

 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
func main() {
    fmt.Println("Starting up")
    changes := make(chan int)
    go waitForChange(changes)

    ticks := time.Tick(5 * time.Minute)
    for {
        err := filepath.Walk("/home/scanner/incoming", sendFile)
        if err != nil {
            fmt.Println(err)
        }
        select {
        case <-changes:
            fmt.Println("File changed, looking for emails to send")
        case <-ticks:
            fmt.Println("5m has passed, looking for emails to send")
        }
    }
}

We start up, print a nice message and make a channel, which is a Go structure that allows us to pass data, in this case integers, between different threads of the program. We call it changes because it will notify us every time there has been a filesystem change.

The go keyword on line 99 starts waitForChange on a separate thread, which is a good thing for us, because you will later see that it runs an infinite loop. We pass it the channel so that it can notify us when it sees a change.

On line 101 we get a Ticker channel, which will receive a signal every five minutes. Since I don’t completely trust that I will be notified every time a file changes, every once in a while we want to look through the directories to see if we find anything.

Starting at line 102, we have an infinite loop here in the main thread. This starts by mailing out any files that are waiting. Then we get to the select statement, which pauses and listens to both the changes channel, and the ticks channel. The somewhat strange arrow syntax means that we are attempting to read values from the channel. If we wanted, we could assign the values we read to a variable and do something with them, but we don’t care what is on the channels. As soon as another thread writes to one of these channels (whichever channel comes first), we write the corresponding log statement and then continue back to the top of the for loop, which mails out any files we find, and then goes back to waiting for action on the channels.

By having the two channels, we have programmed the logic to walk the filesystem every time we see a change and every 5 minutes, but, crucially, never more than once at a time. In reality, the change watcher is very reliable, and the email generally arrives seconds after the paper comes out of the scanner.

83
84
85
86
87
88
89
90
91
92
93
94
func waitForChange(c chan<- int) {
    defer close(c)
    for {
        cmd := exec.Command("/usr/bin/inotifywait", "-r", "-e", "CLOSE_WRITE", "/home/scanner/incoming")
        err := cmd.Run()
        if err != nil {
            fmt.Printf("%v\n", err)
            time.Sleep(5 * time.Minute)
        }
        c <- 0
    }
}

Here is the waitForChange function, which just calls inotifywait. This in turn runs until someone writes a file and then exits. At this point, our function writes 0 into the channel, which kicks the main thread into action. Meanwhile this thread calls inotifywait again, to begin waiting for the next change.

56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
func sendFile(path string, info os.FileInfo, err error) error {
    if err != nil {
        return err
    }
    if info.IsDir() {
        return nil
    }
    to, err := getToAddress(path)
    if err != nil {
        return err
    }
    isOpen, err := fileIsOpen(path)
    if err != nil {
        return err
    }
    if isOpen {
        return fmt.Errorf("Skipping %v because it is opened by another process\n", path)
    }
    if err := sendEmail(to, path); err != nil {
        return err
    }
    if err := os.Remove(path); err != nil {
        return err
    }
    return nil
}

This sendFile function is called on every file in the directory tree. This is where Go gets annoyingly verbose. So much error handling! But fairly straightforward. As we walk the tree, we skip directories, send out emails if we have a file, and then delete the file after we send it.

44
45
46
47
48
49
50
51
52
53
54
func fileIsOpen(path string) (bool, error) {
    cmd := exec.Command("/usr/bin/lsof", "-t", path)
    err := cmd.Run()
    if err == nil {
        return true, nil
    }
    if exitError, ok := err.(*exec.ExitError); ok && exitError.ExitCode() == 1 {
        return false, nil
    }
    return true, err
}

This fileIsOpen function wasn’t there at first, but my early tries sent out files that were still being uploaded. Live and learn.

26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func sendEmail(to string, doc string) error {
    msg := gomail.NewMessage()
    msg.SetHeader("From", "scanner@xxxx")
    msg.SetHeader("To", to)
    msg.SetHeader("Subject", "Here is your scanned document")
    msg.SetBody("text/plain", "")
    msg.Attach(doc)

    n := gomail.NewDialer("smtp.example.com", 465, "user", "pass")

    if err := n.DialAndSend(msg); err != nil {
        return err
    }

    fmt.Printf("Sent %v to %v\n", doc, to)
    return nil
}

It is relatively simple to send an email using this third-party gomail package. And it isn’t malformed like the scanner’s attempts to send email!

12
13
14
15
16
17
18
19
20
21
22
23
24
func getToAddress(path string) (string, error) {
    var addresses = map[string]string{
        "amy":        "xxx@yyyy",
        "nathan":     "xxx@wwww",
    }

    _, lastDirName := filepath.Split(filepath.Dir(path))
    to, ok := addresses[lastDirName]
    if !ok {
        return "", fmt.Errorf("No email address available for %v", lastDirName)
    }
    return to, nil
}

This is a relatively simple function that decides who to email to based on the folder that the file is in. This is the abbreviated version; my older kids also have emails configured here.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import (
    "fmt"
    gomail "gopkg.in/gomail.v2"
    "os"
    "os/exec"
    "path/filepath"
    "time"
)

Finally, the most boring part of all, the import section.