├── .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 | --------------------------------------------------------------------------------