about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/command/cmd_ac.c13
-rw-r--r--src/command/cmd_defs.c134
-rw-r--r--src/command/cmd_funcs.c489
-rw-r--r--src/command/cmd_funcs.h5
-rw-r--r--src/common.c131
-rw-r--r--src/common.h4
-rw-r--r--src/config/files.h19
-rw-r--r--src/config/preferences.c62
-rw-r--r--src/config/preferences.h2
-rw-r--r--src/omemo/crypto.c111
-rw-r--r--src/omemo/crypto.h9
-rw-r--r--src/omemo/omemo.c135
-rw-r--r--src/omemo/omemo.h10
-rw-r--r--src/tools/aesgcm_download.c190
-rw-r--r--src/tools/aesgcm_download.h66
-rw-r--r--src/tools/http_common.c75
-rw-r--r--src/tools/http_common.h44
-rw-r--r--src/tools/http_download.c236
-rw-r--r--src/tools/http_download.h66
-rw-r--r--src/tools/http_upload.c125
-rw-r--r--src/tools/http_upload.h7
-rw-r--r--src/ui/console.c21
-rw-r--r--src/ui/notifier.c4
-rw-r--r--src/ui/window.c4
-rw-r--r--src/ui/window.h2
25 files changed, 1531 insertions, 433 deletions
diff --git a/src/command/cmd_ac.c b/src/command/cmd_ac.c
index 3aeedc60..f7414bad 100644
--- a/src/command/cmd_ac.c
+++ b/src/command/cmd_ac.c
@@ -193,7 +193,6 @@ static Autocomplete otr_sendfile_ac;
 static Autocomplete omemo_ac;
 static Autocomplete omemo_log_ac;
 static Autocomplete omemo_policy_ac;
-static Autocomplete omemo_sendfile_ac;
 #endif
 static Autocomplete connect_property_ac;
 static Autocomplete tls_property_ac;
@@ -683,7 +682,6 @@ cmd_ac_init(void)
     autocomplete_add(omemo_ac, "clear_device_list");
     autocomplete_add(omemo_ac, "policy");
     autocomplete_add(omemo_ac, "char");
-    autocomplete_add(omemo_ac, "sendfile");
 
     omemo_log_ac = autocomplete_new();
     autocomplete_add(omemo_log_ac, "on");
@@ -694,10 +692,6 @@ cmd_ac_init(void)
     autocomplete_add(omemo_policy_ac, "manual");
     autocomplete_add(omemo_policy_ac, "automatic");
     autocomplete_add(omemo_policy_ac, "always");
-
-    omemo_sendfile_ac = autocomplete_new();
-    autocomplete_add(omemo_sendfile_ac, "on");
-    autocomplete_add(omemo_sendfile_ac, "off");
 #endif
 
     connect_property_ac = autocomplete_new();
@@ -1292,7 +1286,6 @@ cmd_ac_reset(ProfWin* window)
     autocomplete_reset(omemo_ac);
     autocomplete_reset(omemo_log_ac);
     autocomplete_reset(omemo_policy_ac);
-    autocomplete_reset(omemo_sendfile_ac);
 #endif
     autocomplete_reset(connect_property_ac);
     autocomplete_reset(tls_property_ac);
@@ -1450,7 +1443,6 @@ cmd_ac_uninit(void)
     autocomplete_free(omemo_ac);
     autocomplete_free(omemo_log_ac);
     autocomplete_free(omemo_policy_ac);
-    autocomplete_free(omemo_sendfile_ac);
 #endif
     autocomplete_free(connect_property_ac);
     autocomplete_free(tls_property_ac);
@@ -2510,11 +2502,6 @@ _omemo_autocomplete(ProfWin* window, const char* const input, gboolean previous)
         return found;
     }
 
-    found = autocomplete_param_with_ac(input, "/omemo sendfile", omemo_sendfile_ac, TRUE, previous);
-    if (found) {
-        return found;
-    }
-
     jabber_conn_status_t conn_status = connection_get_status();
 
     if (conn_status == JABBER_CONNECTED) {
diff --git a/src/command/cmd_defs.c b/src/command/cmd_defs.c
index 51772a27..5058a573 100644
--- a/src/command/cmd_defs.c
+++ b/src/command/cmd_defs.c
@@ -2274,55 +2274,52 @@ static struct cmd_t command_defs[] = {
     },
 
     { "/omemo",
-      parse_args, 1, 3, NULL,
-      CMD_SUBFUNCS(
-              { "gen", cmd_omemo_gen },
-              { "log", cmd_omemo_log },
-              { "start", cmd_omemo_start },
-              { "end", cmd_omemo_end },
-              { "trust", cmd_omemo_trust },
-              { "untrust", cmd_omemo_untrust },
-              { "fingerprint", cmd_omemo_fingerprint },
-              { "char", cmd_omemo_char },
-              { "policy", cmd_omemo_policy },
-              { "clear_device_list", cmd_omemo_clear_device_list },
-              { "sendfile", cmd_omemo_sendfile })
-      CMD_NOMAINFUNC
-      CMD_TAGS(
-              CMD_TAG_CHAT,
-              CMD_TAG_UI)
-      CMD_SYN(
-              "/omemo gen",
-              "/omemo log on|off|redact",
-              "/omemo start [<contact>]",
-              "/omemo trust [<contact>] <fingerprint>",
-              "/omemo end",
-              "/omemo fingerprint [<contact>]",
-              "/omemo char <char>",
-              "/omemo policy manual|automatic|always",
-              "/omemo sendfile on|off",
-              "/omemo clear_device_list")
-      CMD_DESC(
-              "OMEMO commands to manage keys, and perform encryption during chat sessions.")
-      CMD_ARGS(
-              { "gen", "Generate OMEMO crytographic materials for current account." },
-              { "start [<contact>]", "Start an OMEMO session with contact, or current recipient if omitted." },
-              { "end", "End the current OMEMO session." },
-              { "log on|off", "Enable or disable plaintext logging of OMEMO encrypted messages." },
-              { "log redact", "Log OMEMO encrypted messages, but replace the contents with [redacted]. This is the default." },
-              { "fingerprint [<contact>]", "Show contact fingerprints, or current recipient if omitted." },
-              { "char <char>", "Set the character to be displayed next to OMEMO encrypted messages." },
-              { "policy manual", "Set the global OMEMO policy to manual, OMEMO sessions must be started manually." },
-              { "policy automatic", "Set the global OMEMO policy to opportunistic, an OMEMO session will be attempted upon starting a conversation." },
-              { "policy always", "Set the global OMEMO policy to always, an error will be displayed if an OMEMO session cannot be initiated upon starting a conversation." },
-              { "sendfile on|off", "Allow /sendfile to send unencrypted files while in an OMEMO session." },
-              { "clear_device_list", "Clear your own device list on server side. Each client will reannounce itself when connected back." })
-      CMD_EXAMPLES(
-              "/omemo gen",
-              "/omemo start odin@valhalla.edda",
-              "/omemo trust c4f9c875-144d7a3b-0c4a05b6-ca3be51a-a037f329-0bd3ae62-07f99719-55559d2a",
-              "/omemo untrust loki@valhalla.edda c4f9c875-144d7a3b-0c4a05b6-ca3be51a-a037f329-0bd3ae62-07f99719-55559d2a",
-              "/omemo char *")
+        parse_args, 1, 3, NULL,
+        CMD_SUBFUNCS(
+            { "gen", cmd_omemo_gen },
+            { "log", cmd_omemo_log },
+            { "start", cmd_omemo_start },
+            { "end", cmd_omemo_end },
+            { "trust", cmd_omemo_trust },
+            { "untrust", cmd_omemo_untrust },
+            { "fingerprint", cmd_omemo_fingerprint },
+            { "char", cmd_omemo_char },
+            { "policy", cmd_omemo_policy },
+            { "clear_device_list", cmd_omemo_clear_device_list })
+        CMD_NOMAINFUNC
+        CMD_TAGS(
+            CMD_TAG_CHAT,
+            CMD_TAG_UI)
+        CMD_SYN(
+            "/omemo gen",
+            "/omemo log on|off|redact",
+            "/omemo start [<contact>]",
+            "/omemo trust [<contact>] <fingerprint>",
+            "/omemo end",
+            "/omemo fingerprint [<contact>]",
+            "/omemo char <char>",
+            "/omemo policy manual|automatic|always",
+            "/omemo clear_device_list")
+        CMD_DESC(
+            "OMEMO commands to manage keys, and perform encryption during chat sessions.")
+        CMD_ARGS(
+            { "gen",                     "Generate OMEMO crytographic materials for current account." },
+            { "start [<contact>]",       "Start an OMEMO session with contact, or current recipient if omitted." },
+            { "end",                     "End the current OMEMO session." },
+            { "log on|off",              "Enable or disable plaintext logging of OMEMO encrypted messages." },
+            { "log redact",              "Log OMEMO encrypted messages, but replace the contents with [redacted]. This is the default." },
+            { "fingerprint [<contact>]", "Show contact fingerprints, or current recipient if omitted." },
+            { "char <char>",             "Set the character to be displayed next to OMEMO encrypted messages." },
+            { "policy manual",           "Set the global OMEMO policy to manual, OMEMO sessions must be started manually." },
+            { "policy automatic",        "Set the global OMEMO policy to opportunistic, an OMEMO session will be attempted upon starting a conversation." },
+            { "policy always",           "Set the global OMEMO policy to always, an error will be displayed if an OMEMO session cannot be initiated upon starting a conversation." },
+            { "clear_device_list",       "Clear your own device list on server side. Each client will reannounce itself when connected back."})
+        CMD_EXAMPLES(
+            "/omemo gen",
+            "/omemo start odin@valhalla.edda",
+            "/omemo trust c4f9c875-144d7a3b-0c4a05b6-ca3be51a-a037f329-0bd3ae62-07f99719-55559d2a",
+            "/omemo untrust loki@valhalla.edda c4f9c875-144d7a3b-0c4a05b6-ca3be51a-a037f329-0bd3ae62-07f99719-55559d2a",
+            "/omemo char *")
     },
 
     { "/save",
@@ -2377,7 +2374,7 @@ static struct cmd_t command_defs[] = {
               "Settings for consistent color generation for nicks (XEP-0392). Including corrections for Color Vision Deficiencies. "
               "Your terminal needs to support 256 colors.")
       CMD_ARGS(
-              { "on|off|redgreen|blue", "Enable or disable nick colorization for MUC nicks. 'redgreen' is for people with red/green blindess and 'blue' for people with blue blindness." },
+              { "on|off|redgreen|blue", "Enable or disable nick colorization for MUC nicks. 'redgreen' is for people with red/green blindness and 'blue' for people with blue blindness." },
               { "own on|off", "Enable color generation for own nick. If disabled the color from the color from the theme ('me') will get used." })
       CMD_EXAMPLES(
               "/color off",
@@ -2496,26 +2493,35 @@ static struct cmd_t command_defs[] = {
 
     { "/executable",
       parse_args, 2, 4, &cons_executable_setting,
-      CMD_NOSUBFUNCS
-      CMD_MAINFUNC(cmd_executable)
+      CMD_SUBFUNCS(
+              { "avatar",  cmd_executable_avatar },
+              { "urlopen", cmd_executable_urlopen },
+              { "urlsave", cmd_executable_urlsave })
+      CMD_NOMAINFUNC
       CMD_TAGS(
               CMD_TAG_DISCOVERY)
       CMD_SYN(
               "/executable avatar <cmd>",
-              "/executable urlopen (<fileType>|DEF <require_save> <cmd>",
-              "/executable urlsave (<protocol>|DEF) <cmd>")
-      CMD_DESC(
-              "Configure executable that should be called upon a certain command."
-              "Default is xdg-open.")
-      CMD_ARGS(
-              { "avatar", "Set executable that is run in /avatar open. Use your favourite image viewer." },
-              { "urlopen", "Set executable that is run in /url open for a given file type. It may be your favorite browser or a specific viewer. Use DEF to set default command for undefined file type." },
-              { "urlsave", "Set executable that is run in /url save for a given protocol. Use your favourite downloader. Use DEF to set default command for undefined protocol." })
+              "/executable urlopen set <cmdtemplate>",
+              "/executable urlopen default",
+              "/executable urlsave set <cmdtemplate>",
+              "/executable urlsave default")
+      CMD_DESC(
+              "Configure executable that should be called upon a certain command.")
+      CMD_ARGS(
+              { "avatar", "Set executable that is run by /avatar open. Use your favorite image viewer." },
+              { "urlopen set", "Set executable that is run by /url open. It may be your favorite browser or a specific viewer." },
+              { "urlopen default", "Restore to default settings." },
+              { "urlsave set", "Set executable that is run by /url save. It may be your favorite downloader.'" },
+              { "urlsave default", "Use the built-in download method for saving." })
       CMD_EXAMPLES(
               "/executable avatar xdg-open",
-              "/executable urlopen DEF false \"xdg-open %u\"",
-              "/executable urlopen html false \"firefox %u\"",
-              "/executable urlsave aesgcm \"omut -d -o %p %u\"")
+              "/executable urlopen set \"xdg-open %u\"",
+              "/executable urlopen set \"firefox %u\"",
+              "/executable urlopen default",
+              "/executable urlsave set \"wget %u -O %p\"",
+              "/executable urlsave set \"curl %u -o %p\"",
+              "/executable urlsave default")
     },
 
     { "/url",
diff --git a/src/command/cmd_funcs.c b/src/command/cmd_funcs.c
index 0837f630..68153270 100644
--- a/src/command/cmd_funcs.c
+++ b/src/command/cmd_funcs.c
@@ -4,6 +4,7 @@
  *
  * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  * Copyright (C) 2019 Michael Vetter <jubalh@iodoru.org>
+ * Copyright (C) 2020 William Wennerström <william@wstrm.dev>
  *
  * This file is part of Profanity.
  *
@@ -59,6 +60,7 @@
 #include "command/cmd_funcs.h"
 #include "command/cmd_defs.h"
 #include "command/cmd_ac.h"
+#include "config/files.h"
 #include "config/accounts.h"
 #include "config/account.h"
 #include "config/preferences.h"
@@ -67,6 +69,7 @@
 #include "config/scripts.h"
 #include "event/client_events.h"
 #include "tools/http_upload.h"
+#include "tools/http_download.h"
 #include "tools/autocomplete.h"
 #include "tools/parser.h"
 #include "tools/bookmark_ignore.h"
@@ -94,6 +97,7 @@
 #ifdef HAVE_OMEMO
 #include "omemo/omemo.h"
 #include "xmpp/omemo.h"
+#include "tools/aesgcm_download.h"
 #endif
 
 #ifdef HAVE_GTK
@@ -1086,7 +1090,7 @@ _writecsv(int fd, const char* const str)
     size_t len = strlen(str);
     char* s = malloc(2 * len * sizeof(char));
     char* c = s;
-    for (int i =0; i < strlen(str); i++) {
+    for (int i = 0; i < strlen(str); i++) {
         if (str[i] != '"')
             *c++ = str[i];
         else {
@@ -4805,95 +4809,153 @@ cmd_disco(ProfWin* window, const char* const command, gchar** args)
     return TRUE;
 }
 
+// TODO: Move this into its own tools such as HTTPUpload or AESGCMDownload.
+#ifdef HAVE_OMEMO
+char*
+_add_omemo_stream(int* fd, FILE** fh, char** err)
+{
+    // Create temporary file for writing ciphertext.
+    int tmpfd;
+    char* tmpname = NULL;
+    if ((tmpfd = g_file_open_tmp("profanity.XXXXXX", &tmpname, NULL)) == -1) {
+        *err = "Unable to create temporary file for encrypted transfer.";
+        return NULL;
+    }
+    FILE* tmpfh = fdopen(tmpfd, "wb");
+
+    // The temporary ciphertext file should be removed after it has
+    // been closed.
+    remove(tmpname);
+    free(tmpname);
+
+    int crypt_res;
+    char* fragment;
+    fragment = omemo_encrypt_file(*fh, tmpfh, file_size(*fd), &crypt_res);
+    if (crypt_res != 0) {
+        fclose(tmpfh);
+        return NULL;
+    }
+
+    // Force flush as the upload will read from the same stream.
+    fflush(tmpfh);
+    rewind(tmpfh);
+
+    fclose(*fh); // Also closes descriptor.
+
+    // Switch original stream with temporary ciphertext stream.
+    *fd = tmpfd;
+    *fh = tmpfh;
+
+    return fragment;
+}
+#endif
+
 gboolean
 cmd_sendfile(ProfWin* window, const char* const command, gchar** args)
 {
     jabber_conn_status_t conn_status = connection_get_status();
-    char* filename = args[0];
+    gchar* filename;
+    char* alt_scheme = NULL;
+    char* alt_fragment = NULL;
 
     // expand ~ to $HOME
-    if (filename[0] == '~' && filename[1] == '/') {
-        if (asprintf(&filename, "%s/%s", getenv("HOME"), filename + 2) == -1) {
-            return TRUE;
-        }
-    } else {
-        filename = strdup(filename);
+    filename = get_expanded_path(args[0]);
+
+    if (access(filename, R_OK) != 0) {
+        cons_show_error("Uploading '%s' failed: File not found!", filename);
+        goto out;
+    }
+
+    if (!is_regular_file(filename)) {
+        cons_show_error("Uploading '%s' failed: Not a file!", filename);
+        goto out;
     }
 
     if (conn_status != JABBER_CONNECTED) {
         cons_show("You are not currently connected.");
-        free(filename);
-        return TRUE;
+        goto out;
     }
 
     if (window->type != WIN_CHAT && window->type != WIN_PRIVATE && window->type != WIN_MUC) {
         cons_show_error("Unsupported window for file transmission.");
-        free(filename);
-        return TRUE;
+        goto out;
     }
 
+    int fd;
+    if ((fd = open(filename, O_RDONLY)) == -1) {
+        cons_show_error("Unable to open file descriptor for '%s'.", filename);
+        goto out;
+    }
+
+    FILE* fh = fdopen(fd, "rb");
+
     switch (window->type) {
     case WIN_MUC:
-    {
-        ProfMucWin* mucwin = (ProfMucWin*)window;
-        assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
-
-        // only omemo, no pgp/otr available in MUCs
-        if (mucwin->is_omemo && !prefs_get_boolean(PREF_OMEMO_SENDFILE)) {
-            cons_show_error("Uploading unencrypted files disabled. See /omemo sendfile, /otr sendfile, /pgp sendfile.");
-            win_println(window, THEME_ERROR, "-", "Sending encrypted files via http_upload is not possible yet.");
-            free(filename);
-            return TRUE;
-        }
-        break;
-    }
     case WIN_CHAT:
     {
         ProfChatWin* chatwin = (ProfChatWin*)window;
-        assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK);
 
-        if ((chatwin->is_omemo && !prefs_get_boolean(PREF_OMEMO_SENDFILE))
-            || (chatwin->pgp_send && !prefs_get_boolean(PREF_PGP_SENDFILE))
-            || (chatwin->is_otr && !prefs_get_boolean(PREF_OTR_SENDFILE))) {
-            cons_show_error("Uploading unencrypted files disabled. See /omemo sendfile, /otr sendfile, /pgp sendfile.");
-            win_println(window, THEME_ERROR, "-", "Sending encrypted files via http_upload is not possible yet.");
-            free(filename);
-            return TRUE;
+#ifdef HAVE_OMEMO
+        if (chatwin->is_omemo) {
+            char* err = NULL;
+            alt_scheme = OMEMO_AESGCM_URL_SCHEME;
+            alt_fragment = _add_omemo_stream(&fd, &fh, &err);
+            if (err != NULL) {
+                cons_show_error(err);
+                win_println(window, THEME_ERROR, "-", err);
+                goto out;
+            }
+            break;
+        }
+#endif
+
+        if (window->type == WIN_CHAT) {
+            assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK);
+            if ((chatwin->pgp_send && !prefs_get_boolean(PREF_PGP_SENDFILE))
+                || (chatwin->is_otr && !prefs_get_boolean(PREF_OTR_SENDFILE))) {
+                cons_show_error("Uploading unencrypted files disabled. See /otr sendfile or /pgp sendfile.");
+                win_println(window, THEME_ERROR, "-", "Sending encrypted files via http_upload is not possible yet.");
+                goto out;
+            }
         }
         break;
     }
-    case WIN_PRIVATE:
-    {
-        //we don't support encryption in private muc windows
-        break;
-    }
+    case WIN_PRIVATE: // We don't support encryption in private MUC windows.
     default:
         cons_show_error("Unsupported window for file transmission.");
-        free(filename);
-        return TRUE;
-    }
-
-    if (access(filename, R_OK) != 0) {
-        cons_show_error("Uploading '%s' failed: File not found!", filename);
-        free(filename);
-        return TRUE;
-    }
-
-    if (!is_regular_file(filename)) {
-        cons_show_error("Uploading '%s' failed: Not a file!", filename);
-        free(filename);
-        return TRUE;
+        goto out;
     }
 
     HTTPUpload* upload = malloc(sizeof(HTTPUpload));
     upload->window = window;
 
-    upload->filename = filename;
-    upload->filesize = file_size(filename);
+    upload->filename = strdup(filename);
+    upload->filehandle = fh;
+    upload->filesize = file_size(fd);
     upload->mime_type = file_mime_type(filename);
 
+    if (alt_scheme != NULL) {
+        upload->alt_scheme = strdup(alt_scheme);
+    } else {
+        upload->alt_scheme = NULL;
+    }
+
+    if (alt_fragment != NULL) {
+        upload->alt_fragment = strdup(alt_fragment);
+    } else {
+        upload->alt_fragment = NULL;
+    }
+
     iq_http_upload_request(upload);
 
+out:
+#ifdef HAVE_OMEMO
+    if (alt_fragment != NULL)
+        omemo_free(alt_fragment);
+#endif
+    if (filename != NULL)
+        free(filename);
+
     return TRUE;
 }
 
@@ -8773,19 +8835,6 @@ cmd_omemo_policy(ProfWin* window, const char* const command, gchar** args)
 }
 
 gboolean
-cmd_omemo_sendfile(ProfWin* window, const char* const command, gchar** args)
-{
-#ifdef HAVE_OMEMO
-    _cmd_set_boolean_preference(args[1], command, "Sending unencrypted files in an OMEMO session via /sendfile", PREF_OMEMO_SENDFILE);
-
-    return TRUE;
-#else
-    cons_show("This version of Profanity has not been built with OMEMO support enabled");
-    return TRUE;
-#endif
-}
-
-gboolean
 cmd_save(ProfWin* window, const char* const command, gchar** args)
 {
     log_info("Saving preferences to configuration file");
@@ -9005,6 +9054,57 @@ cmd_slashguard(ProfWin* window, const char* const command, gchar** args)
     return TRUE;
 }
 
+#ifdef HAVE_OMEMO
+void
+_url_aesgcm_method(ProfWin* window, const char* cmd_template, const char* url, const char* filename)
+{
+    AESGCMDownload* download = malloc(sizeof(AESGCMDownload));
+    download->window = window;
+    download->url = strdup(url);
+    download->filename = strdup(filename);
+    if (cmd_template != NULL) {
+        download->cmd_template = strdup(cmd_template);
+    } else {
+        download->cmd_template = NULL;
+    }
+
+    pthread_create(&(download->worker), NULL, &aesgcm_file_get, download);
+    aesgcm_download_add_download(download);
+}
+#endif
+
+void
+_url_http_method(ProfWin* window, const char* cmd_template, const char* url, const char* filename)
+{
+
+    HTTPDownload* download = malloc(sizeof(HTTPDownload));
+    download->window = window;
+    download->url = strdup(url);
+    download->filename = strdup(filename);
+    if (cmd_template != NULL) {
+        download->cmd_template = strdup(cmd_template);
+    } else {
+        download->cmd_template = NULL;
+    }
+
+    pthread_create(&(download->worker), NULL, &http_file_get, download);
+    http_download_add_download(download);
+}
+
+void
+_url_external_method(const char* cmd_template, const char* url, const char* filename)
+{
+    gchar** argv = format_call_external_argv(cmd_template, url, filename);
+
+    if (!call_external(argv, NULL, NULL)) {
+        cons_show_error("Unable to call external executable for url: check the logs for more information.");
+    } else {
+        cons_show("URL '%s' has been called with '%s'.", url, cmd_template);
+    }
+
+    g_strfreev(argv);
+}
+
 gboolean
 cmd_url_open(ProfWin* window, const char* const command, gchar** args)
 {
@@ -9013,91 +9113,45 @@ cmd_url_open(ProfWin* window, const char* const command, gchar** args)
         return TRUE;
     }
 
-    if (args[1] == NULL) {
+    gchar* url = args[1];
+    if (url == NULL) {
         cons_bad_cmd_usage(command);
         return TRUE;
     }
 
-    gboolean require_save = false;
+    gchar* scheme = NULL;
+    char* cmd_template = NULL;
+    char* filename = NULL;
 
-    gchar* fileStart = g_strrstr(args[1], "/");
-    if (fileStart == NULL) {
+    scheme = g_uri_parse_scheme(url);
+    if (scheme == NULL) {
         cons_show("URL '%s' is not valid.", args[1]);
-        return TRUE;
-    }
-
-    fileStart++;
-    if (((char*)(fileStart - 2))[0] == '/' && ((char*)(fileStart - 3))[0] == ':') {
-        // If the '/' is last character of the '://' string, there will be no suffix
-        // Therefore, it is considered that there is no file name in the URL and
-        // fileStart is set to the end of the URL.
-        fileStart = args[1] + strlen(args[1]);
+        goto out;
     }
 
-    gchar* suffix = NULL;
-    gchar* suffixStart = g_strrstr(fileStart, ".");
-    if (suffixStart != NULL) {
-        suffixStart++;
-        gchar* suffixEnd = g_strrstr(suffixStart, "#");
-        if (suffixEnd == NULL) {
-            suffix = g_strdup(suffixStart);
-        } else {
-            suffix = g_strndup(suffixStart, suffixEnd - suffixStart);
-        }
-    }
-
-    gchar** suffix_cmd_pref = prefs_get_string_list_with_option(PREF_URL_OPEN_CMD, NULL);
-    if (suffix != NULL) {
-        gchar* lowercase_suffix = g_ascii_strdown(suffix, -1);
-        g_strfreev(suffix_cmd_pref);
-        suffix_cmd_pref = prefs_get_string_list_with_option(PREF_URL_OPEN_CMD, lowercase_suffix);
-        g_free(lowercase_suffix);
-        g_free(suffix);
-    }
-
-    if (0 == g_strcmp0(suffix_cmd_pref[0], "true")) {
-        require_save = true;
+    cmd_template = prefs_get_string(PREF_URL_OPEN_CMD);
+    if (cmd_template == NULL) {
+        cons_show("No default `url open` command found in executables preferences.");
+        goto out;
     }
 
-    gchar* suffix_cmd = g_strdup(suffix_cmd_pref[1]);
-    g_strfreev(suffix_cmd_pref);
-
-    gchar* scheme = g_uri_parse_scheme(args[1]);
+#ifdef HAVE_OMEMO
+    // OMEMO URLs (aesgcm://) must be saved and decrypted before being opened.
     if (0 == g_strcmp0(scheme, "aesgcm")) {
-        require_save = true;
-    }
-    g_free(scheme);
-
-    if (require_save) {
-        gchar* save_args[] = { "open", args[1], "/tmp/profanity.tmp", NULL };
-        cmd_url_save(window, command, save_args);
+        filename = unique_filename_from_url(url, files_get_data_path(DIR_DOWNLOADS));
+        _url_aesgcm_method(window, cmd_template, url, filename);
+        goto out;
     }
+#endif
 
-    gchar** argv = g_strsplit(suffix_cmd, " ", 0);
-    guint num_args = 0;
-    while (argv[num_args]) {
-        if (0 == g_strcmp0(argv[num_args], "%u")) {
-            g_free(argv[num_args]);
-            if (require_save) {
-                argv[num_args] = g_strdup("/tmp/profanity.tmp");
-            } else {
-                argv[num_args] = g_strdup(args[1]);
-            }
-            break;
-        }
-        num_args++;
-    }
+    _url_external_method(cmd_template, url, NULL);
 
-    if (!call_external(argv, NULL, NULL)) {
-        cons_show_error("Unable to open url: check the logs for more information.");
-    }
+out:
 
-    if (require_save) {
-        g_unlink("/tmp/profanity.tmp");
-    }
+    free(cmd_template);
+    free(filename);
 
-    g_strfreev(argv);
-    g_free(suffix_cmd);
+    g_free(scheme);
 
     return TRUE;
 }
@@ -9106,7 +9160,7 @@ gboolean
 cmd_url_save(ProfWin* window, const char* const command, gchar** args)
 {
     if (window->type != WIN_CHAT && window->type != WIN_MUC && window->type != WIN_PRIVATE) {
-        cons_show("url save not supported in this window");
+        cons_show_error("`/url save` is not supported in this window.");
         return TRUE;
     }
 
@@ -9115,121 +9169,104 @@ cmd_url_save(ProfWin* window, const char* const command, gchar** args)
         return TRUE;
     }
 
-    gchar* uri = args[1];
-    gchar* target_path = g_strdup(args[2]);
-
-    GFile* file = g_file_new_for_uri(uri);
-
-    gchar* target_dir = NULL;
-    gchar* base_name = NULL;
-
-    if (target_path == NULL) {
-        target_dir = g_strdup("./");
-        base_name = g_file_get_basename(file);
-        if (0 == g_strcmp0(base_name, ".")) {
-            g_free(base_name);
-            base_name = g_strdup("saved_url_content.html");
-        }
-        target_path = g_strconcat(target_dir, base_name, NULL);
-    }
-
-    if (g_file_test(target_path, G_FILE_TEST_EXISTS) && g_file_test(target_path, G_FILE_TEST_IS_DIR)) {
-        target_dir = g_strdup(target_path);
-        base_name = g_file_get_basename(file);
-        g_free(target_path);
-        target_path = g_strconcat(target_dir, "/", base_name, NULL);
-    }
-
-    g_object_unref(file);
-    file = NULL;
+    gchar* url = args[1];
+    gchar* path = g_strdup(args[2]);
+    gchar* scheme = NULL;
+    char* filename = NULL;
+    char* cmd_template = NULL;
 
-    if (base_name == NULL) {
-        base_name = g_path_get_basename(target_path);
-        target_dir = g_path_get_dirname(target_path);
+    scheme = g_uri_parse_scheme(url);
+    if (scheme == NULL) {
+        cons_show("URL '%s' is not valid.", args[1]);
+        goto out;
     }
 
-    if (!g_file_test(target_dir, G_FILE_TEST_EXISTS) || !g_file_test(target_dir, G_FILE_TEST_IS_DIR)) {
-        cons_show("%s does not exist or is not a directory.", target_dir);
-        g_free(target_path);
-        g_free(target_dir);
-        g_free(base_name);
-        return TRUE;
+    filename = unique_filename_from_url(url, path);
+    if (filename == NULL) {
+        cons_show("Failed to generate unique filename"
+                  "from URL '%s' for path '%s'",
+                  url, path);
+        goto out;
     }
 
-    gchar* scheme = g_uri_parse_scheme(uri);
-    if (scheme == NULL) {
-        cons_show("URL '%s' is not valid.", uri);
-        g_free(target_path);
-        g_free(target_dir);
-        g_free(base_name);
-        return TRUE;
+    cmd_template = prefs_get_string(PREF_URL_SAVE_CMD);
+    if (cmd_template == NULL && (g_strcmp0(scheme, "http") == 0 || g_strcmp0(scheme, "https") == 0)) {
+        _url_http_method(window, cmd_template, url, filename);
+#ifdef HAVE_OMEMO
+    } else if (g_strcmp0(scheme, "aesgcm") == 0) {
+        _url_aesgcm_method(window, cmd_template, url, filename);
+#endif
+    } else if (cmd_template != NULL) {
+        _url_external_method(cmd_template, url, filename);
+    } else {
+        cons_show_error("No download method defined for the scheme '%s'.", scheme);
     }
 
-    gchar* scheme_cmd = NULL;
+out:
 
-    if (0 == g_strcmp0(scheme, "http")
-        || 0 == g_strcmp0(scheme, "https")
-        || 0 == g_strcmp0(scheme, "aesgcm")) {
-        scheme_cmd = prefs_get_string_with_option(PREF_URL_SAVE_CMD, scheme);
-    }
+    free(filename);
+    free(cmd_template);
 
     g_free(scheme);
+    g_free(path);
 
-    gchar** argv = g_strsplit(scheme_cmd, " ", 0);
-    g_free(scheme_cmd);
+    return TRUE;
+}
 
-    guint num_args = 0;
-    while (argv[num_args]) {
-        if (0 == g_strcmp0(argv[num_args], "%u")) {
-            g_free(argv[num_args]);
-            argv[num_args] = g_strdup(uri);
-        } else if (0 == g_strcmp0(argv[num_args], "%p")) {
-            g_free(argv[num_args]);
-            argv[num_args] = target_path;
-        }
-        num_args++;
+gboolean
+cmd_executable_avatar(ProfWin* window, const char* const command, gchar** args)
+{
+    prefs_set_string(PREF_AVATAR_CMD, args[1]);
+    cons_show("`avatar` command set to invoke '%s'", args[1]);
+    return TRUE;
+}
+
+gboolean
+cmd_executable_urlopen(ProfWin* window, const char* const command, gchar** args)
+{
+    guint num_args = g_strv_length(args);
+    if (num_args < 2) {
+        cons_bad_cmd_usage(command);
+        return TRUE;
     }
 
-    if (!call_external(argv, NULL, NULL)) {
-        cons_show_error("Unable to save url: check the logs for more information.");
+    if (g_strcmp0(args[1], "set") == 0 && num_args >= 3) {
+        gchar* str = g_strjoinv(" ", &args[2]);
+        prefs_set_string(PREF_URL_OPEN_CMD, str);
+        cons_show("`url open` command set to invoke '%s'", str);
+        g_free(str);
+
+    } else if (g_strcmp0(args[1], "default") == 0) {
+        prefs_set_string(PREF_URL_SAVE_CMD, NULL);
+        gchar* def = prefs_get_string(PREF_URL_SAVE_CMD);
+        cons_show("`url open` command set to invoke %s (default)", def);
+        g_free(def);
     } else {
-        cons_show("URL '%s' has been saved into '%s'.", uri, target_path);
+        cons_bad_cmd_usage(command);
     }
 
-    g_free(target_dir);
-    g_free(base_name);
-    g_strfreev(argv);
-
     return TRUE;
 }
 
 gboolean
-cmd_executable(ProfWin* window, const char* const command, gchar** args)
+cmd_executable_urlsave(ProfWin* window, const char* const command, gchar** args)
 {
-    if (g_strcmp0(args[0], "avatar") == 0) {
-        prefs_set_string(PREF_AVATAR_CMD, args[1]);
-        cons_show("Avatar command set to: %s", args[1]);
-    } else if (g_strcmp0(args[0], "urlopen") == 0) {
-        if (g_strv_length(args) < 4) {
-            cons_bad_cmd_usage(command);
-            return TRUE;
-        }
 
-        gchar* str = g_strjoinv(" ", &args[3]);
-        const gchar* const list[] = { args[2], str, NULL };
-        prefs_set_string_list_with_option(PREF_URL_OPEN_CMD, args[1], list);
-        cons_show("`url open` command set to: %s for %s files", str, args[1]);
-        g_free(str);
-    } else if (g_strcmp0(args[0], "urlsave") == 0) {
-        if (g_strv_length(args) < 3) {
-            cons_bad_cmd_usage(command);
-            return TRUE;
-        }
+    guint num_args = g_strv_length(args);
+    if (num_args < 2) {
+        cons_bad_cmd_usage(command);
+        return TRUE;
+    }
 
+    if (g_strcmp0(args[1], "set") == 0 && num_args >= 3) {
         gchar* str = g_strjoinv(" ", &args[2]);
-        prefs_set_string_with_option(PREF_URL_SAVE_CMD, args[1], str);
-        cons_show("`url save` command set to: %s for scheme %s", str, args[1]);
+        prefs_set_string(PREF_URL_SAVE_CMD, str);
+        cons_show("`url save` command set to invoke '%s'", str);
         g_free(str);
+
+    } else if (g_strcmp0(args[1], "default") == 0) {
+        prefs_set_string(PREF_URL_SAVE_CMD, NULL);
+        cons_show("`url save` will use built-in download method (default)");
     } else {
         cons_bad_cmd_usage(command);
     }
diff --git a/src/command/cmd_funcs.h b/src/command/cmd_funcs.h
index 92c81364..4955972c 100644
--- a/src/command/cmd_funcs.h
+++ b/src/command/cmd_funcs.h
@@ -223,7 +223,6 @@ gboolean cmd_omemo_trust(ProfWin* window, const char* const command, gchar** arg
 gboolean cmd_omemo_untrust(ProfWin* window, const char* const command, gchar** args);
 gboolean cmd_omemo_policy(ProfWin* window, const char* const command, gchar** args);
 gboolean cmd_omemo_clear_device_list(ProfWin* window, const char* const command, gchar** args);
-gboolean cmd_omemo_sendfile(ProfWin* window, const char* const command, gchar** args);
 
 gboolean cmd_save(ProfWin* window, const char* const command, gchar** args);
 gboolean cmd_reload(ProfWin* window, const char* const command, gchar** args);
@@ -238,6 +237,8 @@ gboolean cmd_slashguard(ProfWin* window, const char* const command, gchar** args
 gboolean cmd_serversoftware(ProfWin* window, const char* const command, gchar** args);
 gboolean cmd_url_open(ProfWin* window, const char* const command, gchar** args);
 gboolean cmd_url_save(ProfWin* window, const char* const command, gchar** args);
-gboolean cmd_executable(ProfWin* window, const char* const command, gchar** args);
+gboolean cmd_executable_avatar(ProfWin* window, const char* const command, gchar** args);
+gboolean cmd_executable_urlopen(ProfWin* window, const char* const command, gchar** args);
+gboolean cmd_executable_urlsave(ProfWin* window, const char* const command, gchar** args);
 
 #endif
diff --git a/src/common.c b/src/common.c
index c0bd6525..8dbf1619 100644
--- a/src/common.c
+++ b/src/common.c
@@ -33,6 +33,9 @@
  * source files in the program, then also delete it here.
  *
  */
+
+#define _GNU_SOURCE 1
+
 #include "config.h"
 
 #include <errno.h>
@@ -555,3 +558,131 @@ call_external(gchar** argv, gchar*** const output_ptr, gchar*** const error_ptr)
 
     return TRUE;
 }
+
+gchar**
+format_call_external_argv(const char* template, const char* url, const char* filename)
+{
+    gchar** argv = g_strsplit(template, " ", 0);
+
+    guint num_args = 0;
+    while (argv[num_args]) {
+        if (0 == g_strcmp0(argv[num_args], "%u") && url != NULL) {
+            g_free(argv[num_args]);
+            argv[num_args] = g_strdup(url);
+        } else if (0 == g_strcmp0(argv[num_args], "%p") && filename != NULL) {
+            g_free(argv[num_args]);
+            argv[num_args] = strdup(filename);
+        }
+        num_args++;
+    }
+
+    return argv;
+}
+
+gchar*
+_unique_filename(const char* filename)
+{
+    gchar* unique = g_strdup(filename);
+
+    unsigned int i = 0;
+    while (g_file_test(unique, G_FILE_TEST_EXISTS)) {
+        free(unique);
+
+        if (i > 1000) { // Give up after 1000 attempts.
+            return NULL;
+        }
+
+        if (asprintf(&unique, "%s.%u", filename, i) < 0) {
+            return NULL;
+        }
+
+        i++;
+    }
+
+    return unique;
+}
+
+bool
+_has_directory_suffix(const char* path)
+{
+    return (g_str_has_suffix(path, ".")
+            || g_str_has_suffix(path, "..")
+            || g_str_has_suffix(path, G_DIR_SEPARATOR_S));
+}
+
+char*
+_basename_from_url(const char* url)
+{
+    const char* default_name = "index";
+
+    GFile* file = g_file_new_for_commandline_arg(url);
+    char* basename = g_file_get_basename(file);
+
+    if (_has_directory_suffix(basename)) {
+        g_free(basename);
+        basename = strdup(default_name);
+    }
+
+    g_object_unref(file);
+
+    return basename;
+}
+
+gchar*
+get_expanded_path(const char *path)
+{
+    GString* exp_path = g_string_new("");
+    gchar *result;
+
+    if (strlen(path) >= 2 && path[0] == '~' && path[1] == '/') {
+        g_string_printf(exp_path, "%s/%s", getenv("HOME"), path+2);
+    } else {
+        g_string_printf(exp_path, "%s", path+2);
+    }
+
+    result = exp_path->str;
+    g_string_free(exp_path, FALSE);
+
+    return result;
+}
+
+gchar*
+unique_filename_from_url(const char* url, const char* path)
+{
+    gchar *realpath;
+
+    // Default to './' as path when none has been provided.
+    if (path == NULL) {
+        realpath = strdup("./");
+    } else {
+        realpath = get_expanded_path(path);
+    }
+
+    // Resolves paths such as './../.' for path.
+    GFile* target = g_file_new_for_commandline_arg(realpath);
+    gchar* filename = NULL;
+
+    if (_has_directory_suffix(realpath) || g_file_test(realpath, G_FILE_TEST_IS_DIR)) {
+        // The target should be used as a directory. Assume that the basename
+        // should be derived from the URL.
+        char* basename = _basename_from_url(url);
+        filename = g_build_filename(g_file_peek_path(target), basename, NULL);
+        g_free(basename);
+    } else {
+        // Just use the target as filename.
+        filename = g_build_filename(g_file_peek_path(target), NULL);
+    }
+
+    gchar* unique_filename = _unique_filename(filename);
+    if (unique_filename == NULL) {
+        g_free(filename);
+        g_free(realpath);
+        return NULL;
+    }
+
+    g_object_unref(target);
+    g_free(filename);
+    g_free(realpath);
+
+    return unique_filename;
+}
diff --git a/src/common.h b/src/common.h
index 13332f7a..44a61a79 100644
--- a/src/common.h
+++ b/src/common.h
@@ -105,5 +105,9 @@ void get_file_paths_recursive(const char* directory, GSList** contents);
 char* get_random_string(int length);
 
 gboolean call_external(gchar** argv, gchar*** const output_ptr, gchar*** const error_ptr);
+gchar** format_call_external_argv(const char* template, const char* url, const char* filename);
+
+gchar* unique_filename_from_url(const char* url, const char* path);
+gchar* get_expanded_path(const char *path);
 
 #endif
diff --git a/src/config/files.h b/src/config/files.h
index d5c96b0f..42499663 100644
--- a/src/config/files.h
+++ b/src/config/files.h
@@ -48,15 +48,16 @@
 #define FILE_PROFANITY_IDENTIFIER     "profident"
 #define FILE_BOOKMARK_AUTOJOIN_IGNORE "bookmark_ignore"
 
-#define DIR_THEMES   "themes"
-#define DIR_ICONS    "icons"
-#define DIR_SCRIPTS  "scripts"
-#define DIR_CHATLOGS "chatlogs"
-#define DIR_OTR      "otr"
-#define DIR_PGP      "pgp"
-#define DIR_OMEMO    "omemo"
-#define DIR_PLUGINS  "plugins"
-#define DIR_DATABASE "database"
+#define DIR_THEMES    "themes"
+#define DIR_ICONS     "icons"
+#define DIR_SCRIPTS   "scripts"
+#define DIR_CHATLOGS  "chatlogs"
+#define DIR_OTR       "otr"
+#define DIR_PGP       "pgp"
+#define DIR_OMEMO     "omemo"
+#define DIR_PLUGINS   "plugins"
+#define DIR_DATABASE  "database"
+#define DIR_DOWNLOADS "downloads"
 
 void files_create_directories(void);
 
diff --git a/src/config/preferences.c b/src/config/preferences.c
index e4aebdb5..98ac5a0d 100644
--- a/src/config/preferences.c
+++ b/src/config/preferences.c
@@ -81,7 +81,6 @@ static const char* _get_group(preference_t pref);
 static const char* _get_key(preference_t pref);
 static gboolean _get_default_boolean(preference_t pref);
 static char* _get_default_string(preference_t pref);
-static char** _get_default_string_list(preference_t pref);
 
 static void
 _prefs_load(void)
@@ -171,7 +170,7 @@ _prefs_load(void)
         value = g_string_append(value, val);
         value = g_string_append(value, " %u;");
 
-        g_key_file_set_locale_string(prefs, PREF_GROUP_EXECUTABLES, "url.open.cmd", "DEF", value->str);
+        g_key_file_set_locale_string(prefs, PREF_GROUP_EXECUTABLES, "url.open.cmd", "*", value->str);
         g_key_file_remove_key(prefs, PREF_GROUP_LOGGING, "urlopen.cmd", NULL);
 
         g_string_free(value, TRUE);
@@ -183,6 +182,11 @@ _prefs_load(void)
         g_key_file_set_string(prefs, PREF_GROUP_EXECUTABLES, "avatar.cmd", value);
         g_key_file_remove_key(prefs, PREF_GROUP_LOGGING, "avatar.cmd", NULL);
     }
+    
+    // 0.10 will have omemo media sharing. so disabling of senfile introduced in 0.9 is not needed (#1270)
+    if (g_key_file_has_key(prefs, PREF_GROUP_OMEMO, "sendfile", NULL)) {
+        g_key_file_remove_key(prefs, PREF_GROUP_OMEMO, "sendfile", NULL);
+    }
 
     _save_prefs();
 
@@ -530,7 +534,7 @@ prefs_get_string_with_option(preference_t pref, gchar* option)
 
     if (result == NULL) {
         // check for user set default
-        result = g_key_file_get_locale_string(prefs, group, key, "DEF", NULL);
+        result = g_key_file_get_locale_string(prefs, group, key, "*", NULL);
         if (result == NULL) {
             if (def) {
                 // use hardcoded profanity default
@@ -544,33 +548,6 @@ prefs_get_string_with_option(preference_t pref, gchar* option)
     return result;
 }
 
-gchar**
-prefs_get_string_list_with_option(preference_t pref, gchar* option)
-{
-    const char* group = _get_group(pref);
-    const char* key = _get_key(pref);
-    char** def = _get_default_string_list(pref);
-
-    gchar** result = g_key_file_get_locale_string_list(prefs, group, key, option, NULL, NULL);
-    if (result) {
-        g_strfreev(def);
-        return result;
-    }
-
-    result = g_key_file_get_string_list(prefs, group, key, NULL, NULL);
-    if (result) {
-        g_strfreev(def);
-        return result;
-    }
-
-    if (def) {
-        return def;
-    } else {
-        g_strfreev(def);
-        return NULL;
-    }
-}
-
 void
 prefs_set_string(preference_t pref, char* value)
 {
@@ -1925,7 +1902,6 @@ _get_group(preference_t pref)
         return PREF_GROUP_PLUGINS;
     case PREF_OMEMO_LOG:
     case PREF_OMEMO_POLICY:
-    case PREF_OMEMO_SENDFILE:
         return PREF_GROUP_OMEMO;
     default:
         return NULL;
@@ -2172,8 +2148,6 @@ _get_key(preference_t pref)
         return "log";
     case PREF_OMEMO_POLICY:
         return "policy";
-    case PREF_OMEMO_SENDFILE:
-        return "sendfile";
     case PREF_CORRECTION_ALLOW:
         return "correction.allow";
     case PREF_AVATAR_CMD:
@@ -2321,26 +2295,10 @@ _get_default_string(preference_t pref)
         return "false";
     case PREF_AVATAR_CMD:
         return "xdg-open";
-    case PREF_URL_SAVE_CMD:
-        return "curl -o %p %u";
-    default:
-        return NULL;
-    }
-}
-
-// the default setting for a string list type preference
-// if it is not specified in .profrc
-static char**
-_get_default_string_list(preference_t pref)
-{
-    char** str_array = NULL;
-
-    switch (pref) {
     case PREF_URL_OPEN_CMD:
-        str_array = g_malloc0(3);
-        str_array[0] = g_strdup("false");
-        str_array[1] = g_strdup("xdg-open %u");
-        return str_array;
+        return "xdg-open %u";
+    case PREF_URL_SAVE_CMD:
+        return NULL; // Default to built-in method.
     default:
         return NULL;
     }
diff --git a/src/config/preferences.h b/src/config/preferences.h
index a9261853..bfad7d6b 100644
--- a/src/config/preferences.h
+++ b/src/config/preferences.h
@@ -165,7 +165,6 @@ typedef enum {
     PREF_STATUSBAR_ROOM,
     PREF_OMEMO_LOG,
     PREF_OMEMO_POLICY,
-    PREF_OMEMO_SENDFILE,
     PREF_OCCUPANTS_WRAP,
     PREF_CORRECTION_ALLOW,
     PREF_AVATAR_CMD,
@@ -321,7 +320,6 @@ gboolean prefs_get_boolean(preference_t pref);
 void prefs_set_boolean(preference_t pref, gboolean value);
 char* prefs_get_string(preference_t pref);
 char* prefs_get_string_with_option(preference_t pref, gchar* option);
-gchar** prefs_get_string_list_with_option(preference_t pref, gchar* option);
 void prefs_set_string(preference_t pref, char* value);
 void prefs_set_string_with_option(preference_t pref, char* option, char* value);
 void prefs_set_string_list_with_option(preference_t pref, char* option, const gchar* const* values);
diff --git a/src/omemo/crypto.c b/src/omemo/crypto.c
index 380551ad..a4a2d5fc 100644
--- a/src/omemo/crypto.c
+++ b/src/omemo/crypto.c
@@ -35,12 +35,14 @@
 #include <assert.h>
 #include <signal/signal_protocol.h>
 #include <signal/signal_protocol_types.h>
-#include <gcrypt.h>
 
 #include "log.h"
 #include "omemo/omemo.h"
 #include "omemo/crypto.h"
 
+#define AES256_GCM_TAG_LENGTH  16
+#define AES256_GCM_BUFFER_SIZE 1024
+
 int
 omemo_crypto_init(void)
 {
@@ -373,3 +375,110 @@ out:
     gcry_cipher_close(hd);
     return res;
 }
+
+gcry_error_t
+aes256gcm_crypt_file(FILE* in, FILE* out, off_t file_size,
+                     unsigned char key[], unsigned char nonce[], bool encrypt)
+{
+
+    if (!gcry_control(GCRYCTL_INITIALIZATION_FINISHED_P)) {
+        fputs("libgcrypt has not been initialized\n", stderr);
+        abort();
+    }
+
+    if (!encrypt) {
+        file_size -= AES256_GCM_TAG_LENGTH;
+    }
+
+    gcry_error_t res;
+    gcry_cipher_hd_t hd;
+
+    res = gcry_cipher_open(&hd, GCRY_CIPHER_AES256, GCRY_CIPHER_MODE_GCM,
+                           GCRY_CIPHER_SECURE);
+    if (res != GPG_ERR_NO_ERROR) {
+        goto out;
+    }
+
+    res = gcry_cipher_setkey(hd, key, OMEMO_AESGCM_KEY_LENGTH);
+    if (res != GPG_ERR_NO_ERROR) {
+        goto out;
+    }
+
+    res = gcry_cipher_setiv(hd, nonce, OMEMO_AESGCM_NONCE_LENGTH);
+    if (res != GPG_ERR_NO_ERROR) {
+        goto out;
+    }
+
+    unsigned char buffer[AES256_GCM_BUFFER_SIZE];
+
+    int bytes = 0;
+    off_t bytes_read = 0, bytes_available = 0, read_size = 0;
+    while (bytes_read < file_size) {
+        bytes_available = file_size - bytes_read;
+        if (!bytes_available || ferror(in) != 0) {
+            break;
+        }
+
+        if (bytes_available < AES256_GCM_BUFFER_SIZE) {
+            read_size = bytes_available;
+            gcry_cipher_final(hd); // Signal last round of bytes.
+        } else {
+            read_size = AES256_GCM_BUFFER_SIZE;
+        }
+
+        bytes = fread(buffer, 1, read_size, in);
+        bytes_read += bytes;
+
+        if (encrypt) {
+            res = gcry_cipher_encrypt(hd, buffer, bytes, NULL, 0);
+        } else {
+            res = gcry_cipher_decrypt(hd, buffer, bytes, NULL, 0);
+        }
+
+        if (res != GPG_ERR_NO_ERROR) {
+            goto out;
+        }
+
+        fwrite(buffer, 1, bytes, out);
+    }
+
+    unsigned char tag[AES256_GCM_TAG_LENGTH];
+
+    if (encrypt) {
+        // Append authentication tag at the end of the file.
+        res = gcry_cipher_gettag(hd, tag, AES256_GCM_TAG_LENGTH);
+        if (res != GPG_ERR_NO_ERROR) {
+            goto out;
+        }
+
+        fwrite(tag, 1, AES256_GCM_TAG_LENGTH, out);
+
+    } else {
+        // Read and verify authentication tag stored at the end of the file.
+        bytes = fread(tag, 1, AES256_GCM_TAG_LENGTH, in);
+        res = gcry_cipher_checktag(hd, tag, bytes);
+    }
+
+out:
+    gcry_cipher_close(hd);
+    return res;
+}
+
+char*
+aes256gcm_create_secure_fragment(unsigned char* key, unsigned char* nonce)
+{
+    int key_size = OMEMO_AESGCM_KEY_LENGTH;
+    int nonce_size = OMEMO_AESGCM_NONCE_LENGTH;
+
+    char* fragment = gcry_malloc_secure((nonce_size + key_size) * 2 + 1);
+
+    for (int i = 0; i < nonce_size; i++) {
+        sprintf(&(fragment[i * 2]), "%02x", nonce[i]);
+    }
+
+    for (int i = 0; i < key_size; i++) {
+        sprintf(&(fragment[(i + nonce_size) * 2]), "%02x", key[i]);
+    }
+
+    return fragment;
+}
diff --git a/src/omemo/crypto.h b/src/omemo/crypto.h
index 4fb6283e..5adbffd8 100644
--- a/src/omemo/crypto.h
+++ b/src/omemo/crypto.h
@@ -32,7 +32,10 @@
  * source files in the program, then also delete it here.
  *
  */
+#include <stdio.h>
+#include <stdbool.h>
 #include <signal/signal_protocol_types.h>
+#include <gcrypt.h>
 
 #define AES128_GCM_KEY_LENGTH 16
 #define AES128_GCM_IV_LENGTH  12
@@ -180,3 +183,9 @@ int aes128gcm_decrypt(unsigned char* plaintext,
                       size_t* plaintext_len, const unsigned char* const ciphertext,
                       size_t ciphertext_len, const unsigned char* const iv, size_t iv_len,
                       const unsigned char* const key, const unsigned char* const tag);
+
+gcry_error_t aes256gcm_crypt_file(FILE* in, FILE* out, off_t file_size,
+                                  unsigned char key[], unsigned char nonce[], bool encrypt);
+
+char* aes256gcm_create_secure_fragment(unsigned char* key,
+                                       unsigned char* nonce);
diff --git a/src/omemo/omemo.c b/src/omemo/omemo.c
index c6c34ac1..22ada3a8 100644
--- a/src/omemo/omemo.c
+++ b/src/omemo/omemo.c
@@ -45,7 +45,6 @@
 #include <signal/signal_protocol.h>
 #include <signal/session_builder.h>
 #include <signal/session_cipher.h>
-#include <gcrypt.h>
 
 #include "config/account.h"
 #include "config/files.h"
@@ -62,6 +61,9 @@
 #include "xmpp/roster_list.h"
 #include "xmpp/xmpp.h"
 
+#define AESGCM_URL_NONCE_LEN (2 * OMEMO_AESGCM_NONCE_LENGTH)
+#define AESGCM_URL_KEY_LEN   (2 * OMEMO_AESGCM_KEY_LENGTH)
+
 static gboolean loaded;
 
 static void _generate_pre_keys(int count);
@@ -1653,3 +1655,134 @@ _generate_signed_pre_key(void)
     signal_protocol_signed_pre_key_store_key(omemo_ctx.store, signed_pre_key);
     SIGNAL_UNREF(signed_pre_key);
 }
+
+void
+omemo_free(void* a)
+{
+    gcry_free(a);
+}
+
+char*
+omemo_encrypt_file(FILE* in, FILE* out, off_t file_size, int* gcry_res)
+{
+    unsigned char* key = gcry_random_bytes_secure(
+        OMEMO_AESGCM_KEY_LENGTH,
+        GCRY_VERY_STRONG_RANDOM);
+
+    // Create nonce/IV with random bytes.
+    unsigned char nonce[OMEMO_AESGCM_NONCE_LENGTH];
+    gcry_create_nonce(nonce, OMEMO_AESGCM_NONCE_LENGTH);
+
+    char* fragment = aes256gcm_create_secure_fragment(key, nonce);
+    *gcry_res = aes256gcm_crypt_file(in, out, file_size, key, nonce, true);
+
+    if (*gcry_res != GPG_ERR_NO_ERROR) {
+        gcry_free(fragment);
+        fragment = NULL;
+    }
+
+    gcry_free(key);
+
+    return fragment;
+}
+
+void
+_bytes_from_hex(const char* hex, size_t hex_size,
+                unsigned char* bytes, size_t bytes_size)
+{
+    const unsigned char ht[] = {
+        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // 01234567
+        0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 89:;<=>?
+        0x00, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x00, // @ABCDEFG
+    };
+    const size_t ht_size = sizeof(ht);
+
+    unsigned char b0;
+    unsigned char b1;
+
+    memset(bytes, 0, bytes_size);
+
+    for (int i = 0; (i < hex_size) && (i / 2 < bytes_size); i += 2) {
+        b0 = ((unsigned char)hex[i + 0] & 0x1f) ^ 0x10;
+        b1 = ((unsigned char)hex[i + 1] & 0x1f) ^ 0x10;
+
+        if (b0 <= ht_size && b1 <= ht_size) {
+            bytes[i / 2] = (unsigned char)(ht[b0] << 4) | ht[b1];
+        }
+    }
+}
+
+gcry_error_t
+omemo_decrypt_file(FILE* in, FILE* out, off_t file_size, const char* fragment)
+{
+    char nonce_hex[AESGCM_URL_NONCE_LEN];
+    char key_hex[AESGCM_URL_KEY_LEN];
+
+    const int nonce_pos = 0;
+    const int key_pos = AESGCM_URL_NONCE_LEN;
+
+    memcpy(nonce_hex, &(fragment[nonce_pos]), AESGCM_URL_NONCE_LEN);
+    memcpy(key_hex, &(fragment[key_pos]), AESGCM_URL_KEY_LEN);
+
+    unsigned char nonce[OMEMO_AESGCM_NONCE_LENGTH];
+    unsigned char* key = gcry_malloc_secure(OMEMO_AESGCM_KEY_LENGTH);
+
+    _bytes_from_hex(nonce_hex, AESGCM_URL_NONCE_LEN,
+                    nonce, OMEMO_AESGCM_NONCE_LENGTH);
+    _bytes_from_hex(key_hex, AESGCM_URL_KEY_LEN,
+                    key, OMEMO_AESGCM_KEY_LENGTH);
+
+    gcry_error_t crypt_res;
+    crypt_res = aes256gcm_crypt_file(in, out, file_size, key, nonce, false);
+
+    gcry_free(key);
+
+    return crypt_res;
+}
+
+int
+omemo_parse_aesgcm_url(const char* aesgcm_url,
+                       char** https_url,
+                       char** fragment)
+{
+    CURLUcode ret;
+    CURLU* url = curl_url();
+
+    // Required to allow for the "aesgcm://" scheme that OMEMO Media Sharing
+    // uses.
+    unsigned int curl_flags = CURLU_NON_SUPPORT_SCHEME;
+
+    ret = curl_url_set(url, CURLUPART_URL, aesgcm_url, curl_flags);
+    if (ret) {
+        goto out;
+    }
+
+    ret = curl_url_get(url, CURLUPART_FRAGMENT, fragment, curl_flags);
+    if (ret) {
+        goto out;
+    }
+
+    if (strlen(*fragment) != AESGCM_URL_NONCE_LEN + AESGCM_URL_KEY_LEN) {
+        goto out;
+    }
+
+    // Clear fragment from HTTPS URL as it's not required for download.
+    ret = curl_url_set(url, CURLUPART_FRAGMENT, NULL, curl_flags);
+    if (ret) {
+        goto out;
+    }
+
+    ret = curl_url_set(url, CURLUPART_SCHEME, "https", curl_flags);
+    if (ret) {
+        goto out;
+    }
+
+    ret = curl_url_get(url, CURLUPART_URL, https_url, curl_flags);
+    if (ret) {
+        goto out;
+    }
+
+out:
+    curl_url_cleanup(url);
+    return ret;
+}
diff --git a/src/omemo/omemo.h b/src/omemo/omemo.h
index ecfc42d9..7e7c1f14 100644
--- a/src/omemo/omemo.h
+++ b/src/omemo/omemo.h
@@ -33,6 +33,7 @@
  *
  */
 #include <glib.h>
+#include <gcrypt.h>
 
 #include "ui/ui.h"
 #include "config/account.h"
@@ -40,6 +41,10 @@
 #define OMEMO_ERR_UNSUPPORTED_CRYPTO -10000
 #define OMEMO_ERR_GCRYPT             -20000
 
+#define OMEMO_AESGCM_NONCE_LENGTH AES128_GCM_IV_LENGTH
+#define OMEMO_AESGCM_KEY_LENGTH   32
+#define OMEMO_AESGCM_URL_SCHEME   "aesgcm"
+
 typedef enum {
     PROF_OMEMOPOLICY_MANUAL,
     PROF_OMEMOPOLICY_AUTOMATIC,
@@ -95,3 +100,8 @@ void omemo_start_device_session(const char* const jid, uint32_t device_id, GList
 gboolean omemo_loaded(void);
 char* omemo_on_message_send(ProfWin* win, const char* const message, gboolean request_receipt, gboolean muc, const char* const replace_id);
 char* omemo_on_message_recv(const char* const from, uint32_t sid, const unsigned char* const iv, size_t iv_len, GList* keys, const unsigned char* const payload, size_t payload_len, gboolean muc, gboolean* trusted);
+
+char* omemo_encrypt_file(FILE* in, FILE* out, off_t file_size, int* gcry_res);
+gcry_error_t omemo_decrypt_file(FILE* in, FILE* out, off_t file_size, const char* fragment);
+void omemo_free(void* a);
+int omemo_parse_aesgcm_url(const char* aesgcm_url, char** https_url, char** fragment);
diff --git a/src/tools/aesgcm_download.c b/src/tools/aesgcm_download.c
new file mode 100644
index 00000000..96f8d7e8
--- /dev/null
+++ b/src/tools/aesgcm_download.c
@@ -0,0 +1,190 @@
+/*
+ * aesgcm_download.c
+ * vim: expandtab:ts=4:sts=4:sw=4
+ *
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2020 William Wennerström <william@wstrm.dev>
+ *
+ * This file is part of Profanity.
+ *
+ * Profanity is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Profanity is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Profanity.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * In addition, as a special exception, the copyright holders give permission to
+ * link the code of portions of this program with the OpenSSL library under
+ * certain conditions as described in each individual source file, and
+ * distribute linked combinations including the two.
+ *
+ * You must obey the GNU General Public License in all respects for all of the
+ * code used other than OpenSSL. If you modify file(s) with this exception, you
+ * may extend this exception to your version of the file(s), but you are not
+ * obligated to do so. If you do not wish to do so, delete this exception
+ * statement from your version. If you delete this exception statement from all
+ * source files in the program, then also delete it here.
+ *
+ */
+
+#define _GNU_SOURCE 1
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <curl/curl.h>
+#include <gio/gio.h>
+#include <pthread.h>
+#include <assert.h>
+#include <errno.h>
+
+#include "profanity.h"
+#include "event/client_events.h"
+#include "tools/http_common.h"
+#include "tools/aesgcm_download.h"
+#include "omemo/omemo.h"
+#include "config/preferences.h"
+#include "ui/ui.h"
+#include "ui/window.h"
+#include "common.h"
+
+#define FALLBACK_MSG ""
+
+void*
+aesgcm_file_get(void* userdata)
+{
+    AESGCMDownload* aesgcm_dl = (AESGCMDownload*)userdata;
+
+    char* https_url = NULL;
+    char* fragment = NULL;
+
+    // Convert the aesgcm:// URL to a https:// URL and extract the encoded key
+    // and tag stored in the URL fragment.
+    if (omemo_parse_aesgcm_url(aesgcm_dl->url, &https_url, &fragment) != 0) {
+        http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->url,
+                                   "Download failed: Cannot parse URL '%s'.",
+                                   aesgcm_dl->url);
+        return NULL;
+    }
+
+    // Create a temporary file used for storing the ciphertext that is to be
+    // retrieved from the https:// URL.
+    gchar* tmpname = NULL;
+    gint tmpfd;
+    if ((tmpfd = g_file_open_tmp("profanity.XXXXXX", &tmpname, NULL)) == -1) {
+        http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->url,
+                                   "Downloading '%s' failed: Unable to create "
+                                   "temporary ciphertext file for writing "
+                                   "(%s).",
+                                   https_url, g_strerror(errno));
+        return NULL;
+    }
+
+    // Open the target file for storing the cleartext.
+    FILE* outfh = fopen(aesgcm_dl->filename, "wb");
+    if (outfh == NULL) {
+        http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->url,
+                                   "Downloading '%s' failed: Unable to open "
+                                   "output file at '%s' for writing (%s).",
+                                   https_url, aesgcm_dl->filename,
+                                   g_strerror(errno));
+        return NULL;
+    }
+
+    // We wrap the HTTPDownload tool and use it for retrieving the ciphertext
+    // and storing it in the temporary file previously opened.
+    HTTPDownload* http_dl = malloc(sizeof(HTTPDownload));
+    http_dl->window = aesgcm_dl->window;
+    http_dl->worker = aesgcm_dl->worker;
+    http_dl->url = strdup(https_url);
+    http_dl->filename = strdup(tmpname);
+    http_dl->cmd_template = NULL;
+    aesgcm_dl->http_dl = http_dl;
+
+    http_file_get(http_dl); // TODO(wstrm): Verify result.
+
+    FILE* tmpfh = fopen(tmpname, "rb");
+    if (tmpfh == NULL) {
+        http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->url,
+                                   "Downloading '%s' failed: Unable to open "
+                                   "temporary file at '%s' for reading (%s).",
+                                   aesgcm_dl->url, tmpname,
+                                   g_strerror(errno));
+        return NULL;
+    }
+
+    gcry_error_t crypt_res;
+    crypt_res = omemo_decrypt_file(tmpfh, outfh,
+                                   http_dl->bytes_received, fragment);
+
+    if (fclose(tmpfh) == EOF) {
+        cons_show_error(g_strerror(errno));
+    }
+
+    close(tmpfd);
+    remove(tmpname);
+    g_free(tmpname);
+
+    if (crypt_res != GPG_ERR_NO_ERROR) {
+        http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->url,
+                                   "Downloading '%s' failed: Failed to decrypt "
+                                   "file (%s).",
+                                   https_url, gcry_strerror(crypt_res));
+    }
+
+    if (fclose(outfh) == EOF) {
+        cons_show_error(g_strerror(errno));
+    }
+
+    free(https_url);
+    free(fragment);
+
+    if (aesgcm_dl->cmd_template != NULL) {
+        gchar** argv = format_call_external_argv(aesgcm_dl->cmd_template,
+                                                 aesgcm_dl->filename,
+                                                 aesgcm_dl->filename);
+
+        // TODO: Log the error.
+        if (!call_external(argv, NULL, NULL)) {
+            http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->url,
+                                       "Downloading '%s' failed: Unable to call "
+                                       "command '%s' with file at '%s' (%s).",
+                                       aesgcm_dl->url,
+                                       aesgcm_dl->cmd_template,
+                                       aesgcm_dl->filename,
+                                       "TODO: Log the error");
+        }
+
+        g_strfreev(argv);
+        free(aesgcm_dl->cmd_template);
+    }
+
+    free(aesgcm_dl->filename);
+    free(aesgcm_dl->url);
+    free(aesgcm_dl);
+
+    return NULL;
+}
+
+void
+aesgcm_download_cancel_processes(ProfWin* window)
+{
+    http_download_cancel_processes(window);
+}
+
+void
+aesgcm_download_add_download(AESGCMDownload* aesgcm_dl)
+{
+    http_download_add_download(aesgcm_dl->http_dl);
+}
diff --git a/src/tools/aesgcm_download.h b/src/tools/aesgcm_download.h
new file mode 100644
index 00000000..c0096f1d
--- /dev/null
+++ b/src/tools/aesgcm_download.h
@@ -0,0 +1,66 @@
+/*
+ * aesgcm_download.h
+ * vim: expandtab:ts=4:sts=4:sw=4
+ *
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2020 William Wennerström <william@wstrm.dev>
+ *
+ * This file is part of Profanity.
+ *
+ * Profanity is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Profanity is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Profanity.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * In addition, as a special exception, the copyright holders give permission to
+ * link the code of portions of this program with the OpenSSL library under
+ * certain conditions as described in each individual source file, and
+ * distribute linked combinations including the two.
+ *
+ * You must obey the GNU General Public License in all respects for all of the
+ * code used other than OpenSSL. If you modify file(s) with this exception, you
+ * may extend this exception to your version of the file(s), but you are not
+ * obligated to do so. If you do not wish to do so, delete this exception
+ * statement from your version. If you delete this exception statement from all
+ * source files in the program, then also delete it here.
+ *
+ */
+
+#ifndef TOOLS_AESGCM_DOWNLOAD_H
+#define TOOLS_AESGCM_DOWNLOAD_H
+
+#ifdef PLATFORM_CYGWIN
+#define SOCKET int
+#endif
+
+#include <sys/select.h>
+#include <curl/curl.h>
+#include "tools/http_common.h"
+#include "tools/http_download.h"
+
+#include "ui/win_types.h"
+
+typedef struct aesgcm_download_t
+{
+    char* url;
+    char* filename;
+    char* cmd_template;
+    ProfWin* window;
+    pthread_t worker;
+    HTTPDownload* http_dl;
+} AESGCMDownload;
+
+void* aesgcm_file_get(void* userdata);
+
+void aesgcm_download_cancel_processes(ProfWin* window);
+void aesgcm_download_add_download(AESGCMDownload* download);
+
+#endif
diff --git a/src/tools/http_common.c b/src/tools/http_common.c
new file mode 100644
index 00000000..e066a6f6
--- /dev/null
+++ b/src/tools/http_common.c
@@ -0,0 +1,75 @@
+/*
+ * http_common.c
+ * vim: expandtab:ts=4:sts=4:sw=4
+ *
+ * Copyright (C) 2020 William Wennerström <william@wstrm.dev>
+ *
+ * This file is part of Profanity.
+ *
+ * Profanity is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Profanity is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Profanity.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * In addition, as a special exception, the copyright holders give permission to
+ * link the code of portions of this program with the OpenSSL library under
+ * certain conditions as described in each individual source file, and
+ * distribute linked combinations including the two.
+ *
+ * You must obey the GNU General Public License in all respects for all of the
+ * code used other than OpenSSL. If you modify file(s) with this exception, you
+ * may extend this exception to your version of the file(s), but you are not
+ * obligated to do so. If you do not wish to do so, delete this exception
+ * statement from your version. If you delete this exception statement from all
+ * source files in the program, then also delete it here.
+ *
+ */
+
+#define _GNU_SOURCE 1
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <gio/gio.h>
+
+#include "tools/http_common.h"
+
+#define FALLBACK_MSG ""
+
+void
+http_print_transfer_update(ProfWin* window, char* url, const char* fmt, ...)
+{
+    va_list args;
+
+    va_start(args, fmt);
+    GString* msg = g_string_new(FALLBACK_MSG);
+    g_string_vprintf(msg, fmt, args);
+    va_end(args);
+
+    win_update_entry_message(window, url, msg->str);
+
+    g_string_free(msg, TRUE);
+}
+
+void
+http_print_transfer(ProfWin* window, char* url, const char* fmt, ...)
+{
+    va_list args;
+
+    va_start(args, fmt);
+    GString* msg = g_string_new(FALLBACK_MSG);
+    g_string_vprintf(msg, fmt, args);
+    va_end(args);
+
+    win_print_http_transfer(window, msg->str, url);
+
+    g_string_free(msg, TRUE);
+}
diff --git a/src/tools/http_common.h b/src/tools/http_common.h
new file mode 100644
index 00000000..ac51b5a8
--- /dev/null
+++ b/src/tools/http_common.h
@@ -0,0 +1,44 @@
+/*
+ * http_common.h
+ * vim: expandtab:ts=4:sts=4:sw=4
+ *
+ * Copyright (C) 2020 William Wennerström <william@wstrm.dev>
+ *
+ * This file is part of Profanity.
+ *
+ * Profanity is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Profanity is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Profanity.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * In addition, as a special exception, the copyright holders give permission to
+ * link the code of portions of this program with the OpenSSL library under
+ * certain conditions as described in each individual source file, and
+ * distribute linked combinations including the two.
+ *
+ * You must obey the GNU General Public License in all respects for all of the
+ * code used other than OpenSSL. If you modify file(s) with this exception, you
+ * may extend this exception to your version of the file(s), but you are not
+ * obligated to do so. If you do not wish to do so, delete this exception
+ * statement from your version. If you delete this exception statement from all
+ * source files in the program, then also delete it here.
+ *
+ */
+
+#ifndef TOOLS_HTTP_COMMON_H
+#define TOOLS_HTTP_COMMON_H
+
+#include "ui/window.h"
+
+void http_print_transfer(ProfWin* window, char* url, const char* fmt, ...);
+void http_print_transfer_update(ProfWin* window, char* url, const char* fmt, ...);
+
+#endif
diff --git a/src/tools/http_download.c b/src/tools/http_download.c
new file mode 100644
index 00000000..f97fd704
--- /dev/null
+++ b/src/tools/http_download.c
@@ -0,0 +1,236 @@
+/*
+ * http_download.c
+ * vim: expandtab:ts=4:sts=4:sw=4
+ *
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2020 William Wennerström <william@wstrm.dev>
+ *
+ * This file is part of Profanity.
+ *
+ * Profanity is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Profanity is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Profanity.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * In addition, as a special exception, the copyright holders give permission to
+ * link the code of portions of this program with the OpenSSL library under
+ * certain conditions as described in each individual source file, and
+ * distribute linked combinations including the two.
+ *
+ * You must obey the GNU General Public License in all respects for all of the
+ * code used other than OpenSSL. If you modify file(s) with this exception, you
+ * may extend this exception to your version of the file(s), but you are not
+ * obligated to do so. If you do not wish to do so, delete this exception
+ * statement from your version. If you delete this exception statement from all
+ * source files in the program, then also delete it here.
+ *
+ */
+
+#define _GNU_SOURCE 1
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <curl/curl.h>
+#include <gio/gio.h>
+#include <pthread.h>
+#include <assert.h>
+#include <errno.h>
+
+#include "profanity.h"
+#include "event/client_events.h"
+#include "tools/http_download.h"
+#include "config/preferences.h"
+#include "ui/ui.h"
+#include "ui/window.h"
+#include "common.h"
+
+GSList* download_processes = NULL;
+
+static int
+_xferinfo(void* userdata, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
+{
+    HTTPDownload* download = (HTTPDownload*)userdata;
+
+    pthread_mutex_lock(&lock);
+
+    if (download->cancel) {
+        pthread_mutex_unlock(&lock);
+        return 1;
+    }
+
+    if (download->bytes_received == dlnow) {
+        pthread_mutex_unlock(&lock);
+        return 0;
+    } else {
+        download->bytes_received = dlnow;
+    }
+
+    unsigned int dlperc = 0;
+    if (dltotal != 0) {
+        dlperc = (100 * dlnow) / dltotal;
+    }
+
+    http_print_transfer_update(download->window, download->url,
+                               "Downloading '%s': %d%%", download->url, dlperc);
+
+    pthread_mutex_unlock(&lock);
+
+    return 0;
+}
+
+#if LIBCURL_VERSION_NUM < 0x072000
+static int
+_older_progress(void* p, double dltotal, double dlnow, double ultotal, double ulnow)
+{
+    return _xferinfo(p, (curl_off_t)dltotal, (curl_off_t)dlnow, (curl_off_t)ultotal, (curl_off_t)ulnow);
+}
+#endif
+
+void*
+http_file_get(void* userdata)
+{
+    HTTPDownload* download = (HTTPDownload*)userdata;
+
+    char* err = NULL;
+
+    CURL* curl;
+    CURLcode res;
+
+    download->cancel = 0;
+    download->bytes_received = 0;
+
+    pthread_mutex_lock(&lock);
+    http_print_transfer(download->window, download->url,
+                        "Downloading '%s': 0%%", download->url);
+
+    FILE* outfh = fopen(download->filename, "wb");
+    if (outfh == NULL) {
+        http_print_transfer_update(download->window, download->url,
+                                   "Downloading '%s' failed: Unable to open "
+                                   "output file at '%s' for writing (%s).",
+                                   download->url, download->filename,
+                                   g_strerror(errno));
+        return NULL;
+    }
+
+    char* cert_path = prefs_get_string(PREF_TLS_CERTPATH);
+    pthread_mutex_unlock(&lock);
+
+    curl_global_init(CURL_GLOBAL_ALL);
+    curl = curl_easy_init();
+
+    curl_easy_setopt(curl, CURLOPT_URL, download->url);
+
+#if LIBCURL_VERSION_NUM >= 0x072000
+    curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, _xferinfo);
+    curl_easy_setopt(curl, CURLOPT_XFERINFODATA, download);
+#else
+    curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, _older_progress);
+    curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, download);
+#endif
+    curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
+
+    curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)outfh);
+
+    curl_easy_setopt(curl, CURLOPT_USERAGENT, "profanity");
+
+    if (cert_path) {
+        curl_easy_setopt(curl, CURLOPT_CAPATH, cert_path);
+    }
+
+    if ((res = curl_easy_perform(curl)) != CURLE_OK) {
+        err = strdup(curl_easy_strerror(res));
+    }
+
+    curl_easy_cleanup(curl);
+    curl_global_cleanup();
+
+    if (fclose(outfh) == EOF) {
+        err = strdup(g_strerror(errno));
+    }
+
+    pthread_mutex_lock(&lock);
+    g_free(cert_path);
+    if (err) {
+        if (download->cancel) {
+            http_print_transfer_update(download->window, download->url,
+                                       "Downloading '%s' failed: "
+                                       "Download was canceled",
+                                       download->url);
+        } else {
+            http_print_transfer_update(download->window, download->url,
+                                       "Downloading '%s' failed: %s",
+                                       download->url, err);
+        }
+        free(err);
+    } else {
+        if (!download->cancel) {
+            http_print_transfer_update(download->window, download->url,
+                                       "Downloading '%s': done",
+                                       download->url);
+            win_mark_received(download->window, download->url);
+        }
+    }
+
+    download_processes = g_slist_remove(download_processes, download);
+    pthread_mutex_unlock(&lock);
+
+    if (download->cmd_template != NULL) {
+        gchar** argv = format_call_external_argv(download->cmd_template,
+                                                 download->url,
+                                                 download->filename);
+
+        // TODO: Log the error.
+        if (!call_external(argv, NULL, NULL)) {
+            http_print_transfer_update(download->window, download->url,
+                                       "Downloading '%s' failed: Unable to call "
+                                       "command '%s' with file at '%s' (%s).",
+                                       download->url,
+                                       download->cmd_template,
+                                       download->filename,
+                                       "TODO: Log the error");
+        }
+
+        g_strfreev(argv);
+        free(download->cmd_template);
+    }
+
+    free(download->url);
+    free(download->filename);
+    free(download);
+
+    return NULL;
+}
+
+void
+http_download_cancel_processes(ProfWin* window)
+{
+    GSList* download_process = download_processes;
+    while (download_process) {
+        HTTPDownload* download = download_process->data;
+        if (download->window == window) {
+            download->cancel = 1;
+            break;
+        }
+        download_process = g_slist_next(download_process);
+    }
+}
+
+void
+http_download_add_download(HTTPDownload* download)
+{
+    download_processes = g_slist_append(download_processes, download);
+}
diff --git a/src/tools/http_download.h b/src/tools/http_download.h
new file mode 100644
index 00000000..23344f6c
--- /dev/null
+++ b/src/tools/http_download.h
@@ -0,0 +1,66 @@
+/*
+ * http_download.h
+ * vim: expandtab:ts=4:sts=4:sw=4
+ *
+ * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
+ * Copyright (C) 2020 William Wennerström <william@wstrm.dev>
+ *
+ * This file is part of Profanity.
+ *
+ * Profanity is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Profanity is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Profanity.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * In addition, as a special exception, the copyright holders give permission to
+ * link the code of portions of this program with the OpenSSL library under
+ * certain conditions as described in each individual source file, and
+ * distribute linked combinations including the two.
+ *
+ * You must obey the GNU General Public License in all respects for all of the
+ * code used other than OpenSSL. If you modify file(s) with this exception, you
+ * may extend this exception to your version of the file(s), but you are not
+ * obligated to do so. If you do not wish to do so, delete this exception
+ * statement from your version. If you delete this exception statement from all
+ * source files in the program, then also delete it here.
+ *
+ */
+
+#ifndef TOOLS_HTTP_DOWNLOAD_H
+#define TOOLS_HTTP_DOWNLOAD_H
+
+#ifdef PLATFORM_CYGWIN
+#define SOCKET int
+#endif
+
+#include <sys/select.h>
+#include <curl/curl.h>
+
+#include "ui/win_types.h"
+#include "tools/http_common.h"
+
+typedef struct http_download_t
+{
+    char* url;
+    char* filename;
+    char* cmd_template;
+    curl_off_t bytes_received;
+    ProfWin* window;
+    pthread_t worker;
+    int cancel;
+} HTTPDownload;
+
+void* http_file_get(void* userdata);
+
+void http_download_cancel_processes(ProfWin* window);
+void http_download_add_download(HTTPDownload* download);
+
+#endif
diff --git a/src/tools/http_upload.c b/src/tools/http_upload.c
index 312fad46..fcdd582a 100644
--- a/src/tools/http_upload.c
+++ b/src/tools/http_upload.c
@@ -128,12 +128,41 @@ _data_callback(void* ptr, size_t size, size_t nmemb, void* data)
     return realsize;
 }
 
+int
+format_alt_url(char* original_url, char* new_scheme, char* new_fragment, char** new_url)
+{
+    int ret = 0;
+    CURLU* h = curl_url();
+
+    if ((ret = curl_url_set(h, CURLUPART_URL, original_url, 0)) != 0) {
+        goto out;
+    }
+
+    if (new_scheme != NULL) {
+        if ((ret = curl_url_set(h, CURLUPART_SCHEME, new_scheme, CURLU_NON_SUPPORT_SCHEME)) != 0) {
+            goto out;
+        }
+    }
+
+    if (new_fragment != NULL) {
+        if ((ret = curl_url_set(h, CURLUPART_FRAGMENT, new_fragment, 0)) != 0) {
+            goto out;
+        }
+    }
+
+    ret = curl_url_get(h, CURLUPART_URL, new_url, 0);
+
+out:
+    curl_url_cleanup(h);
+    return ret;
+}
+
 void*
 http_file_put(void* userdata)
 {
     HTTPUpload* upload = (HTTPUpload*)userdata;
 
-    FILE* fd = NULL;
+    FILE* fh = NULL;
 
     char* err = NULL;
     char* content_type_header;
@@ -149,7 +178,7 @@ http_file_put(void* userdata)
     if (asprintf(&msg, "Uploading '%s': 0%%", upload->filename) == -1) {
         msg = strdup(FALLBACK_MSG);
     }
-    win_print_http_upload(upload->window, msg, upload->put_url);
+    win_print_http_transfer(upload->window, msg, upload->put_url);
     free(msg);
 
     char* cert_path = prefs_get_string(PREF_TLS_CERTPATH);
@@ -186,18 +215,13 @@ http_file_put(void* userdata)
 
     curl_easy_setopt(curl, CURLOPT_USERAGENT, "profanity");
 
-    if (!(fd = fopen(upload->filename, "rb"))) {
-        if (asprintf(&err, "failed to open '%s'", upload->filename) == -1) {
-            err = NULL;
-        }
-        goto end;
-    }
+    fh = upload->filehandle;
 
     if (cert_path) {
         curl_easy_setopt(curl, CURLOPT_CAPATH, cert_path);
     }
 
-    curl_easy_setopt(curl, CURLOPT_READDATA, fd);
+    curl_easy_setopt(curl, CURLOPT_READDATA, fh);
     curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)(upload->filesize));
     curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
 
@@ -225,12 +249,11 @@ http_file_put(void* userdata)
 #endif
     }
 
-end:
     curl_easy_cleanup(curl);
     curl_global_cleanup();
     curl_slist_free_all(headers);
-    if (fd) {
-        fclose(fd);
+    if (fh) {
+        fclose(fh);
     }
     free(content_type_header);
     free(output.buffer);
@@ -262,30 +285,42 @@ end:
             win_mark_received(upload->window, upload->put_url);
             free(msg);
 
-            switch (upload->window->type) {
-            case WIN_CHAT:
-            {
-                ProfChatWin* chatwin = (ProfChatWin*)(upload->window);
-                assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK);
-                cl_ev_send_msg(chatwin, upload->get_url, upload->get_url);
-                break;
-            }
-            case WIN_PRIVATE:
-            {
-                ProfPrivateWin* privatewin = (ProfPrivateWin*)(upload->window);
-                assert(privatewin->memcheck == PROFPRIVATEWIN_MEMCHECK);
-                cl_ev_send_priv_msg(privatewin, upload->get_url, upload->get_url);
-                break;
-            }
-            case WIN_MUC:
-            {
-                ProfMucWin* mucwin = (ProfMucWin*)(upload->window);
-                assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
-                cl_ev_send_muc_msg(mucwin, upload->get_url, upload->get_url);
-                break;
-            }
-            default:
-                break;
+            char* url = NULL;
+            if (format_alt_url(upload->get_url, upload->alt_scheme, upload->alt_fragment, &url) != 0) {
+                char* msg;
+                if (asprintf(&msg, "Uploading '%s' failed: Bad URL ('%s')", upload->filename, upload->get_url) == -1) {
+                    msg = strdup(FALLBACK_MSG);
+                }
+                cons_show_error(msg);
+                free(msg);
+            } else {
+                switch (upload->window->type) {
+                case WIN_CHAT:
+                {
+                    ProfChatWin* chatwin = (ProfChatWin*)(upload->window);
+                    assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK);
+                    cl_ev_send_msg(chatwin, url, url);
+                    break;
+                }
+                case WIN_PRIVATE:
+                {
+                    ProfPrivateWin* privatewin = (ProfPrivateWin*)(upload->window);
+                    assert(privatewin->memcheck == PROFPRIVATEWIN_MEMCHECK);
+                    cl_ev_send_priv_msg(privatewin, url, url);
+                    break;
+                }
+                case WIN_MUC:
+                {
+                    ProfMucWin* mucwin = (ProfMucWin*)(upload->window);
+                    assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
+                    cl_ev_send_muc_msg(mucwin, url, url);
+                    break;
+                }
+                default:
+                    break;
+                }
+
+                curl_free(url);
             }
         }
     }
@@ -297,24 +332,26 @@ end:
     free(upload->mime_type);
     free(upload->get_url);
     free(upload->put_url);
+    free(upload->alt_scheme);
+    free(upload->alt_fragment);
     free(upload);
 
     return NULL;
 }
 
 char*
-file_mime_type(const char* const file_name)
+file_mime_type(const char* const filename)
 {
     char* out_mime_type;
     char file_header[FILE_HEADER_BYTES];
-    FILE* fd;
-    if (!(fd = fopen(file_name, "rb"))) {
+    FILE* fh;
+    if (!(fh = fopen(filename, "rb"))) {
         return strdup(FALLBACK_MIMETYPE);
     }
-    size_t file_header_size = fread(file_header, 1, FILE_HEADER_BYTES, fd);
-    fclose(fd);
+    size_t file_header_size = fread(file_header, 1, FILE_HEADER_BYTES, fh);
+    fclose(fh);
 
-    char* content_type = g_content_type_guess(file_name, (unsigned char*)file_header, file_header_size, NULL);
+    char* content_type = g_content_type_guess(filename, (unsigned char*)file_header, file_header_size, NULL);
     if (content_type != NULL) {
         char* mime_type = g_content_type_get_mime_type(content_type);
         out_mime_type = strdup(mime_type);
@@ -327,10 +364,10 @@ file_mime_type(const char* const file_name)
 }
 
 off_t
-file_size(const char* const filename)
+file_size(int filedes)
 {
     struct stat st;
-    stat(filename, &st);
+    fstat(filedes, &st);
     return st.st_size;
 }
 
diff --git a/src/tools/http_upload.h b/src/tools/http_upload.h
index 3838a5e8..4e95d4d8 100644
--- a/src/tools/http_upload.h
+++ b/src/tools/http_upload.h
@@ -48,11 +48,14 @@
 typedef struct http_upload_t
 {
     char* filename;
+    FILE* filehandle;
     off_t filesize;
     curl_off_t bytes_sent;
     char* mime_type;
     char* get_url;
     char* put_url;
+    char* alt_scheme;
+    char* alt_fragment;
     ProfWin* window;
     pthread_t worker;
     int cancel;
@@ -60,8 +63,8 @@ typedef struct http_upload_t
 
 void* http_file_put(void* userdata);
 
-char* file_mime_type(const char* const file_name);
-off_t file_size(const char* const file_name);
+char* file_mime_type(const char* const filename);
+off_t file_size(int filedes);
 
 void http_upload_cancel_processes(ProfWin* window);
 void http_upload_add_upload(HTTPUpload* upload);
diff --git a/src/ui/console.c b/src/ui/console.c
index cb2bb888..306b13d5 100644
--- a/src/ui/console.c
+++ b/src/ui/console.c
@@ -2068,17 +2068,20 @@ cons_correction_setting(void)
 void
 cons_executable_setting(void)
 {
-    char* avatar = prefs_get_string(PREF_AVATAR_CMD);
+    gchar* avatar = prefs_get_string(PREF_AVATAR_CMD);
     cons_show("Default '/avatar open' command (/executable avatar)                      : %s", avatar);
     g_free(avatar);
 
     //TODO: there needs to be a way to get all the "locales"/schemes so we can
-    //display the defualt openers for all filetypes
-    gchar** urlopen = prefs_get_string_list_with_option(PREF_URL_OPEN_CMD, "");
-    cons_show("Default '/url open' command (/executable urlopen)                        : %s", urlopen[1]);
-    g_strfreev(urlopen);
+    //display the default openers for all filetypes
+    gchar* urlopen = prefs_get_string(PREF_URL_OPEN_CMD);
+    cons_show("Default '/url open' command (/executable urlopen)                        : %s", urlopen);
+    g_free(urlopen);
 
-    char* urlsave = prefs_get_string(PREF_URL_SAVE_CMD);
+    gchar* urlsave = prefs_get_string(PREF_URL_SAVE_CMD);
+    if (urlsave == NULL) {
+        urlsave = g_strdup("(built-in)");
+    }
     cons_show("Default '/url save' command (/executable urlsave)                        : %s", urlsave);
     g_free(urlsave);
 }
@@ -2192,12 +2195,6 @@ cons_show_omemo_prefs(void)
     cons_show("OMEMO char (/omemo char)     : %s", ch);
     free(ch);
 
-    if (prefs_get_boolean(PREF_OMEMO_SENDFILE)) {
-        cons_show("Allow sending unencrypted files in an OMEMO session via /sendfile (/omemo sendfile): ON");
-    } else {
-        cons_show("Allow sending unencrypted files in an OMEMO session via /sendfile (/omemo sendfile): OFF");
-    }
-
     cons_alert(NULL);
 }
 
diff --git a/src/ui/notifier.c b/src/ui/notifier.c
index 352351f9..02f7edfe 100644
--- a/src/ui/notifier.c
+++ b/src/ui/notifier.c
@@ -204,11 +204,11 @@ notify(const char* const message, int timeout, const char* const category)
     if (notify_is_initted()) {
         log_debug("Reinitialising libnotify");
         notify_uninit();
-        notify_init("Profanity");
     } else {
         log_debug("Initialising libnotify");
-        notify_init("Profanity");
     }
+    notify_init("Profanity");
+
     if (notify_is_initted()) {
         NotifyNotification* notification;
         notification = notify_notification_new("Profanity", message, NULL);
diff --git a/src/ui/window.c b/src/ui/window.c
index 687af3b2..56f477da 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -1390,7 +1390,7 @@ win_appendln_highlight(ProfWin* window, theme_item_t theme_item, const char* con
 }
 
 void
-win_print_http_upload(ProfWin* window, const char* const message, char* url)
+win_print_http_transfer(ProfWin* window, const char* const message, char* url)
 {
     win_print_outgoing_with_receipt(window, "!", NULL, message, url, NULL);
 }
@@ -1742,7 +1742,7 @@ win_print_trackbar(ProfWin* window)
     wbkgdset(window->layout->win, theme_attrs(THEME_TRACKBAR));
     wattron(window->layout->win, theme_attrs(THEME_TRACKBAR));
 
-    for (int i = 1; i < cols; i++) {
+    for (int i = 1; i <= cols; i++) {
         wprintw(window->layout->win, "-");
     }
 
diff --git a/src/ui/window.h b/src/ui/window.h
index c731d19b..7ff25a87 100644
--- a/src/ui/window.h
+++ b/src/ui/window.h
@@ -72,7 +72,7 @@ void win_println_incoming_muc_msg(ProfWin* window, char* show_char, int flags, c
 void win_print_outgoing_muc_msg(ProfWin* window, char* show_char, const char* const me, const char* const id, const char* const replace_id, const char* const message);
 void win_print_history(ProfWin* window, const ProfMessage* const message);
 
-void win_print_http_upload(ProfWin* window, const char* const message, char* url);
+void win_print_http_transfer(ProfWin* window, const char* const message, char* url);
 
 void win_newline(ProfWin* window);
 void win_redraw(ProfWin* window);