├── .gitignore
├── LICENSE
├── ReadMe.md
├── client
├── config.go
├── encrypt.go
├── net.go
└── tests
│ ├── file.txt
│ └── key.pub
└── sendto.go
/.gitignore:
--------------------------------------------------------------------------------
1 | server/secrets
2 | server/files
3 | server/log/*.log
4 | server/db/backup/*.gz
5 | server/bin
6 | server/public/assets/scripts/app-*
7 | server/public/assets/styles/app-*
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Kenny Grant
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/ReadMe.md:
--------------------------------------------------------------------------------
1 |
Sendto
2 |
3 | Sendto is a quick way for people to send you encrypted files and folders, without knowing anything about encryption, keys or passwords. This project was produced for Gopher Gala as a learning project. I am not a cryptographer, the code has not been audited (though it does use a reliable crypto library, it is possible there are vulnerabilities introduced by the usage), so please proceed at your own risk. I don't yet consider this project ready for production but would like to continue the experiment - contributions or suggestions are welcome.
4 |
5 | * No key generation or passwords. Sendto lets users use your PGP public key to encrypt files for you, and uploads them to the server encrypted for you to download. There is no difficult dance of sending keys or passwords on an insecure channel, or complex software for them to master.
6 | * Files encrypted at all times. Sendto cannot open your files because it only knows about your public key, so it can encrypt but never decrypt. TLS is also used for all connections.
7 | * Open Source. Sendto is completely open source, so that you can verify what happens to your files, and run it on your own server if you prefer self-hosting. Encryption uses the google library: golang.org/x/crypto/openpgp.
8 |
9 | ### Receive files securely
10 |
11 | Just send people a link to your profile, and they can download an app for their platform to send you encrypted files. An example profile is here:
12 |
13 | https://sendto.click/users/demo
14 |
15 | This shows the public key, and a set of download links. Download the binary for your platform (or install from source if you prefer, see below), and then you can send a file to any sendto account for which you know the username, with a command like:
16 |
17 | `sendto demo my/file/or/folder`
18 |
19 | this will send the file to the demo account. You won't be able to download it though, as you need your own profile to access uploaded files. Please only send test files to the demo account.
20 |
21 | Once you've tried the demo, if you have a pgp key, or a keybase.io account, try setting up a user. The server can pull keys automatically from keybase.io so setup is easy. No email is required for sign up at this time. *You MUST use a PGP key*, not any other kind of key.
22 |
23 | Once files are uploaded to your account, you're able to view and download them by logging in. Decryption happens on your machine, so that your private keys are never shared with the server.
24 |
25 |
26 | ### Try it
27 | https://sendto.click
28 |
29 | ### Open source on github
30 | go get github.com/send-to
31 |
32 | This app is open source so that you can build it yourself, and check what it does. If you have Go installed, you can also install the client and server from source with:
33 |
34 | `go get github.com/send-to/sendto`
35 |
36 | and then use the sendto command:
37 |
38 | `sendto help`
39 |
40 | you can host the server yourself if you prefer to have complete control by checking out the server repo:
41 |
42 | `go get github.com/send-to/sendtoserver`
43 |
44 | ### Created by
45 | @kennygrant on twitter, github, keybase.io, sendto.click.
46 | Contributions and pull requests welcome.
47 |
48 | ### Version History
49 |
50 | ##### Version 0.1
51 | * Created for Gopher Gala 2016
52 |
53 | ##### Version 0.1.1
54 | * Added warnings to readme, notes on possible bugs
55 | * Fixed bug - if absolute paths given they should be truncated to the enclosing folder only for zip
56 | * Added more specific warning on config failure
57 |
58 | ##### Version 0.1.2
59 | * Add detailed errors for encryption/zip
60 | * Complain if non-pgp key used
61 |
62 | ### Possible bugs and limitations
63 |
64 | * Signatures are not yet checked, so no authenticity guarantee is given - we should instead sign if private keys are available locally, or consider tie-in with keybase.io.
65 | * The sender username should be set by the user (ideally they should be prompted for it) - this ties in with signing above.
66 | * Would be nice to lean on keybase.io if possible more for authentication/getting keys - can be used as key server just now but is not the default - multiple key server support?
67 | * Keys are cached locally for users (at ~/.sendto), so if you change your key you'd have to remove the prefs locally in order to update it. This needs fixed at some point obviously.
68 | * User enumeration is possible by numeric id, and there are no restrictions on who can send you files - neither is particularly desirable - perhaps consider using email as unique identifier and namespace for users?
69 | * A few usability notes from Matthew - Readme should be updated, zip file for binaries should be named appropriately.
70 | * Times are shown in UTC - I rather like this but can see that others might prefer a local time.
71 |
--------------------------------------------------------------------------------
/client/config.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "os"
8 | "os/user"
9 | "path"
10 | "path/filepath"
11 | )
12 |
13 | // Config holds our client config
14 | var Config map[string]string
15 |
16 | // LoadConfig reads or creates our config file at ~/.sendto/config
17 | func LoadConfig() error {
18 |
19 | // Create our files folder to store files to send
20 | err := createFolder("files")
21 | if err != nil {
22 | return fmt.Errorf("config: create folder error %s", err)
23 | }
24 |
25 | // Create our users folder to store public keys downloaded
26 | err = createFolder("users")
27 | if err != nil {
28 | return fmt.Errorf("config: create folder error %s", err)
29 | }
30 |
31 | // Load our config file (or create a new one with one entry - sender identity)
32 | // First check it exists
33 | file, err := ioutil.ReadFile(configFilePath())
34 | // We tolerate failure here as file may not exist
35 | if err == nil {
36 | err = json.Unmarshal(file, &Config)
37 | if err != nil {
38 | return fmt.Errorf("config: json error %s", err)
39 | }
40 | }
41 |
42 | // If no config exists create a config and save it
43 | if len(Config) == 0 {
44 | err = setupConfig()
45 | if err != nil {
46 | return fmt.Errorf("config: setup error %s", err)
47 | }
48 |
49 | err = SaveConfig()
50 | if err != nil {
51 | return fmt.Errorf("config: save error %s", err)
52 | }
53 |
54 | }
55 |
56 | return nil
57 | }
58 |
59 | // SaveConfig saves our config out to a file at ~/.sendto/config
60 | func SaveConfig() error {
61 | // Write out a json file representing our config map
62 | configJSON, err := json.MarshalIndent(Config, "", "\t")
63 | if err != nil {
64 | return err
65 | }
66 |
67 | // Write the config json file
68 | err = ioutil.WriteFile(configFilePath(), configJSON, os.ModePerm)
69 | if err != nil {
70 | return err
71 | }
72 |
73 | return nil
74 | }
75 |
76 | func setupConfig() error {
77 | Config = make(map[string]string, 0)
78 |
79 | // Use the home dir as a default sender name
80 | name := path.Base(homePath())
81 |
82 | u, err := user.Current()
83 | // Recover gracefully from lack of user.Current() on other platforms
84 | // only set if this is supported
85 | if err == nil && u.Name != "" {
86 | name = u.Name
87 | }
88 |
89 | // Set up a default config, pointing to sendto.click
90 | Config["sender"] = name
91 | Config["keyserver"] = "https://sendto.click/users/%s/key"
92 | Config["server"] = "https://sendto.click"
93 |
94 | fmt.Printf("Setting default config:%v\n", Config)
95 | return nil
96 | }
97 |
98 | func configPath() string {
99 | return filepath.Join(homePath(), ".sendto")
100 | }
101 |
102 | func homePath() string {
103 | home := os.Getenv("HOME")
104 | if home == "" {
105 | home = os.Getenv("USERPROFILE")
106 | }
107 | return home
108 | }
109 |
110 | func configFilePath() string {
111 | return filepath.Join(configPath(), "config.json")
112 | }
113 |
114 | func createFolder(name string) error {
115 | p := filepath.Join(configPath(), name)
116 | return os.MkdirAll(p, os.ModeDir|os.ModePerm)
117 | }
118 |
119 | // fileExists returns true if this file exists
120 | func fileExists(path string) bool {
121 | _, err := os.Stat(path)
122 | if os.IsNotExist(err) {
123 | return false
124 | }
125 | return true
126 | }
127 |
--------------------------------------------------------------------------------
/client/encrypt.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "archive/zip"
5 | "fmt"
6 | "io"
7 | "os"
8 | "path"
9 | "path/filepath"
10 | "strings"
11 | "time"
12 |
13 | "golang.org/x/crypto/openpgp"
14 | "golang.org/x/crypto/openpgp/armor"
15 | "golang.org/x/crypto/openpgp/packet"
16 | )
17 |
18 | // LoadKey loads the key associated with this username,
19 | // first by loooking at ~/.sendto/users/recipient/key.pub
20 | // or if that fails by fetching it from the internet and saving at that location
21 | // it returns the path of the downloaded key file
22 | func LoadKey(recipient string, url string) (string, error) {
23 | fmt.Printf("Loading key for %s...\n", recipient)
24 |
25 | // Define a local keypath of users/name/key.pub
26 | keyPath := filepath.Join(configPath(), "users", recipient, "key.pub")
27 |
28 | // Check if the key file exists at ~/.sendto/users/recipient/key.pub
29 | if !fileExists(keyPath) {
30 | // Make the user directory
31 | createFolder(filepath.Join("users", recipient))
32 |
33 | // Fetch the key from our server
34 | err := DownloadData(url, keyPath)
35 | if err != nil {
36 | return "", err
37 | }
38 |
39 | // Tell user we fetched key
40 | fmt.Printf("Fetched key for user:%s from:%s\n", recipient, url)
41 |
42 | // Print the key for the user as we have fetched it for the first time?
43 | /*
44 | key, err := ioutil.ReadFile(keyPath)
45 | if err != nil {
46 | return "", err
47 | }
48 | */
49 | }
50 |
51 | return keyPath, nil
52 | }
53 |
54 | // EncryptFiles zips then encrypts our arguments (files or folders) using a public key
55 | func EncryptFiles(args []string, recipient string, keyPath string) (string, error) {
56 |
57 | // First open and parse recipient key
58 | publicKey, err := ParsePublicKey(keyPath)
59 | if err != nil {
60 | return "", fmt.Errorf("encrypt: invalid pgp key %s", err)
61 | }
62 |
63 | fmt.Printf("Using key: %x\n", publicKey.PrimaryKey.Fingerprint)
64 |
65 | // Make the user files directory
66 | createFolder(filepath.Join("files", recipient))
67 |
68 | // At present the caller sets the filename - perhaps reconsider as this leaks info - FIXME
69 | name := path.Clean(path.Base(args[0]))
70 |
71 | // Create a file at config/files/recipient/name.zip.gpg
72 | outPath := filepath.Join(configPath(), "files", recipient, fmt.Sprintf("%s.zip.gpg", name))
73 |
74 | // Create the file
75 | out, err := os.Create(outPath)
76 | if err != nil {
77 | return "", fmt.Errorf("encrypt: error on create files %s", err)
78 | }
79 | defer out.Close()
80 |
81 | // Create encryption writer
82 | hints := &openpgp.FileHints{IsBinary: true, FileName: fmt.Sprintf("%s.zip", name), ModTime: time.Now()}
83 | pgpWriter, err := openpgp.Encrypt(out, []*openpgp.Entity{publicKey}, nil, hints, nil)
84 | if err != nil {
85 | return "", fmt.Errorf("encrypt: error creating pgp writer %s", err)
86 | }
87 |
88 | // Now create a zipwriter, which writes to this pgpWriter
89 | zipWriter := zip.NewWriter(pgpWriter)
90 |
91 | // Add the files/folders from our args
92 | for _, arg := range args {
93 |
94 | // For each argument, walk the file path adding files to our zip
95 | err := filepath.Walk(arg, func(p string, info os.FileInfo, err error) error {
96 | if err != nil {
97 | return err
98 | }
99 | if info.IsDir() {
100 | return nil
101 | }
102 |
103 | f, err := os.Open(p)
104 | if err != nil {
105 | return err
106 | }
107 | defer f.Close()
108 |
109 | // Remove the current path from name if supplied
110 | p = strings.Replace(p, arg, "", 1)
111 |
112 | // Support unicode filenames by default
113 | h := &zip.FileHeader{Name: p, Method: zip.Deflate, Flags: 0x800}
114 | z, err := zipWriter.CreateHeader(h)
115 | // Doesn't support unicode file names?
116 | // z, err := zipWriter.Create(p)
117 | if err != nil {
118 | return err
119 | }
120 | io.Copy(z, f)
121 | zipWriter.Flush()
122 | return nil
123 | })
124 | if err != nil {
125 | return "", fmt.Errorf("zip: error creating zip file %s", err)
126 | }
127 |
128 | }
129 | err = zipWriter.Flush()
130 | if err != nil {
131 | return "", fmt.Errorf("zip: error flushing zip file %s", err)
132 | }
133 | err = zipWriter.Close()
134 | if err != nil {
135 | return "", fmt.Errorf("zip: error closing zip file %s", err)
136 | }
137 |
138 | // close the encPipe to finish the process
139 | err = pgpWriter.Close()
140 | if err != nil {
141 | return "", fmt.Errorf("encrypt: error closing pgp writer %s", err)
142 | }
143 |
144 | // Make sure the file path returned uses forward slashes on windows
145 | // - change from mattn moved here
146 | outPath = filepath.ToSlash(outPath)
147 |
148 | return outPath, nil
149 | }
150 |
151 | // ParsePublicKey parses the given public key file
152 | func ParsePublicKey(keyPath string) (*openpgp.Entity, error) {
153 | f, err := os.Open(keyPath)
154 | if err != nil {
155 | return nil, err
156 | }
157 | defer f.Close()
158 |
159 | // Parse our key
160 | key, err := armor.Decode(f)
161 | if err != nil {
162 | return nil, err
163 | }
164 | if key.Type != openpgp.PublicKeyType {
165 | return nil, fmt.Errorf("Key of wrong type:%s", key.Type)
166 | }
167 | r := packet.NewReader(key.Body)
168 | to, err := openpgp.ReadEntity(r)
169 | if err != nil {
170 | return nil, err
171 | }
172 |
173 | return to, nil
174 | }
175 |
176 | // DecryptFiles decrypts then unzips a file using a private key
177 | // and returns the path of the resulting file/folder on success
178 | // zip step should be optional TODO
179 | func DecryptFiles(p string, key string) (string, error) {
180 |
181 | return "", nil
182 | }
183 |
--------------------------------------------------------------------------------
/client/net.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "mime/multipart"
8 | "net/http"
9 | "os"
10 | )
11 |
12 | // DownloadData retrieves a file from the server and saves contents at filePath
13 | func DownloadData(url string, filePath string) error {
14 |
15 | // Fetch the file
16 | resp, err := http.Get(url)
17 | if err != nil {
18 | return err
19 | }
20 | defer resp.Body.Close()
21 |
22 | if resp.StatusCode != http.StatusOK {
23 | return fmt.Errorf("error %d downloading file at %s", resp.StatusCode, url)
24 | }
25 |
26 | // If response OK, create our file with downloaded response body
27 | f, err := os.Create(filePath)
28 | if err != nil {
29 | return err
30 | }
31 | n, err := io.Copy(f, resp.Body)
32 | if err != nil {
33 | return err
34 | }
35 | fmt.Printf("Wrote %d bytes to file %s\n", n, filePath)
36 |
37 | return nil
38 | }
39 |
40 | // PostData sends data to the server
41 | func PostData(sender, recipient, file, url string) error {
42 |
43 | // Prepare a new multipart form writer
44 | var formData bytes.Buffer
45 | w := multipart.NewWriter(&formData)
46 |
47 | // Add the encrypted file
48 | err := addFile(w, file)
49 | if err != nil {
50 | return err
51 | }
52 |
53 | // Add sender identity
54 | err = addField(w, "sender", sender)
55 | if err != nil {
56 | return err
57 | }
58 | // Add recipient identity
59 | err = addField(w, "recipient", recipient)
60 | if err != nil {
61 | return err
62 | }
63 |
64 | // Close the writer
65 | w.Close()
66 |
67 | // Now post the form
68 | req, err := http.NewRequest("POST", url, &formData)
69 | if err != nil {
70 | return err
71 | }
72 | req.Header.Set("Content-Type", w.FormDataContentType())
73 |
74 | resp, err := http.DefaultClient.Do(req)
75 | if err != nil {
76 | return err
77 | }
78 | if resp.StatusCode != http.StatusOK {
79 | return fmt.Errorf("Error status %d posting file to %s\n", resp.StatusCode, url)
80 | }
81 |
82 | //fmt.Printf("File sent to: %s %v\n", url, resp.Body)
83 | fmt.Printf("File sent to: %s\n", url)
84 |
85 | return nil
86 |
87 | }
88 |
89 | // addFile adds a file to this multipart form
90 | func addFile(w *multipart.Writer, filePath string) error {
91 | f, err := os.Open(filePath)
92 | if err != nil {
93 | return err
94 | }
95 | fw, err := w.CreateFormFile("file", filePath)
96 | if err != nil {
97 | return err
98 | }
99 | _, err = io.Copy(fw, f)
100 | if err != nil {
101 | return err
102 | }
103 | return nil
104 | }
105 |
106 | // addField adds a field to the multipart writer
107 | func addField(w *multipart.Writer, k, v string) error {
108 | fw, err := w.CreateFormField(k)
109 | if err != nil {
110 | return err
111 | }
112 | _, err = fw.Write([]byte(v))
113 | if err != nil {
114 | return err
115 | }
116 | return nil
117 | }
118 |
--------------------------------------------------------------------------------
/client/tests/file.txt:
--------------------------------------------------------------------------------
1 | Encrypt me - hello world
--------------------------------------------------------------------------------
/client/tests/key.pub:
--------------------------------------------------------------------------------
1 | -----BEGIN PGP PUBLIC KEY BLOCK-----
2 | Comment: GPGTools - http://gpgtools.org
3 |
4 | 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
50 | sQm+Y3caobPqLlqlNC8/6DEhJqSOpvhoMA==
51 | =ga0U
52 | -----END PGP PUBLIC KEY BLOCK-----
--------------------------------------------------------------------------------
/sendto.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 |
8 | "github.com/send-to/sendto/client"
9 | )
10 |
11 | const (
12 | v = "0.1.2"
13 | )
14 |
15 | func main() {
16 | command := ""
17 | args := os.Args[1:] // remove app path from args
18 |
19 | // We expect either a username or a subcommand and then a set of files in args
20 | if len(args) > 0 {
21 | command = args[0]
22 | args = args[1:]
23 | }
24 |
25 | // Load our configuration
26 | err := client.LoadConfig()
27 | if err != nil {
28 | log.Fatalf("Error reading config: %s", err)
29 | }
30 |
31 | switch command {
32 | case "encrypt", "e":
33 | err = Encrypt(args)
34 | case "decrypt", "d":
35 | err = Decrypt(args)
36 | case "identity", "i":
37 | err = Identity(args)
38 | case "version", "v":
39 | Version()
40 | case "help", "h":
41 | Help()
42 | default:
43 | // Default action is to send to (if we have a username and files)
44 | if len(args) > 0 {
45 | err = SendTo(command, args)
46 | } else {
47 | Help()
48 | }
49 | }
50 |
51 | if err != nil {
52 | log.Fatalf("Sorry, an error occurred sending files:\n%s", err)
53 | }
54 | }
55 |
56 | // Version prints the version of this app
57 | func Version() {
58 | fmt.Printf("\t-----\n\tSend to client - version:%s\n\t-----\n", v)
59 | }
60 |
61 | // Usage returns standard usage as a string
62 | func Usage() string {
63 | return fmt.Sprintf("\tUsage: sendto kennygrant [files] - send files to the username kennygrant\n")
64 | }
65 |
66 | // Help prints the usage and commands
67 | func Help() {
68 | Version()
69 | fmt.Printf(Usage())
70 | fmt.Printf("\t-----\n")
71 | fmt.Printf("\tCommands:\n")
72 | fmt.Printf("\tsendto version - display version\n")
73 | fmt.Printf("\tsendto [username] [files] - encrypt files for a given user\n")
74 | fmt.Printf("\tsendto encrypt [file] - encrypt a file\n")
75 | // fmt.Printf("\tsendto decrypt [file] - decrypt a file\n")
76 | fmt.Printf("\tsendto identity [name] - sets default sender identity\n\n")
77 | }
78 |
79 | // Decrypt files specified, using the user's private key
80 | // TODO: to support decryption we'd need access to private keys, perhaps leave this for hackathon
81 | func Decrypt(args []string) error {
82 | log.Printf("Sorry, this client does not yet support decrypt")
83 |
84 | return nil
85 | }
86 |
87 | // Encrypt the files specified
88 | func Encrypt(args []string) error {
89 |
90 | log.Printf("Sorry, this client does not yet support encryption")
91 | return nil
92 | }
93 |
94 | // SendTo sends files held in args to recipient
95 | func SendTo(recipient string, args []string) error {
96 |
97 | // We expect at least 1 file to send
98 | if len(args) < 1 {
99 | return fmt.Errorf("Not enough arguments - %s", Usage())
100 | }
101 |
102 | // Notify the user that we're starting to send
103 | fmt.Printf("Sending %d %s to %s as %s...\n", len(args), filesString(len(args)), recipient, client.Config["sender"])
104 |
105 | // Fetch the recipient's key (from disk or server)
106 |
107 | // For the moment as a test, use keybase.io, should be using our server
108 | keyURL := fmt.Sprintf(client.Config["keyserver"], recipient)
109 | keyPath, err := client.LoadKey(recipient, keyURL)
110 | if err != nil {
111 | // Warn user in a nicer way here that key could not be found
112 | return fmt.Errorf("Failed to find key:%s", err)
113 | }
114 | fmt.Printf("Loaded key for %s:\n%s\n", recipient, keyPath)
115 |
116 | // Zip and Encrypt our arguments (files or folders) using key
117 | dataPath, err := client.EncryptFiles(args, recipient, keyPath)
118 | if err != nil {
119 | return fmt.Errorf("Failed to encrypt: %s", err)
120 | }
121 |
122 | // Send the file to the recipient on the server
123 | postURL := fmt.Sprintf("%s/files/create", client.Config["server"])
124 |
125 | fmt.Printf("Sending files for %s to %s\n", recipient, postURL)
126 |
127 | err = client.PostData(client.Config["sender"], recipient, dataPath, postURL)
128 | if err != nil {
129 | return fmt.Errorf("Failed to send: %s", err)
130 | }
131 |
132 | return nil
133 | }
134 |
135 | // Identity sets the default sender identity (as opposed to username)
136 | func Identity(args []string) error {
137 | if len(args) < 1 {
138 | return fmt.Errorf("Identity command requires a sender name")
139 | }
140 |
141 | identity := args[0]
142 | client.Config["sender"] = identity
143 |
144 | fmt.Printf("Setting sender identity to:%s\n", identity)
145 |
146 | return client.SaveConfig()
147 | }
148 |
149 | // Perhaps also allow setting default server?
150 |
151 | // Return a nicely formatted string for the word files
152 | func filesString(i int) string {
153 | if i > 1 {
154 | return "files"
155 | }
156 | return "file"
157 | }
158 |
--------------------------------------------------------------------------------