about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--Makefile.am5
-rw-r--r--src/command/cmd_ac.c250
-rw-r--r--src/command/cmd_defs.c136
-rw-r--r--src/command/cmd_funcs.c990
-rw-r--r--src/command/cmd_funcs.h9
-rw-r--r--src/config/files.h1
-rw-r--r--src/config/preferences.c10
-rw-r--r--src/config/preferences.h2
-rw-r--r--src/event/common.c2
-rw-r--r--src/event/server_events.c3
-rw-r--r--src/ui/console.c8
-rw-r--r--src/ui/ui.h8
-rw-r--r--src/ui/vcardwin.c75
-rw-r--r--src/ui/win_types.h12
-rw-r--r--src/ui/window.c189
-rw-r--r--src/ui/window_list.c33
-rw-r--r--src/ui/window_list.h2
-rw-r--r--src/xmpp/stanza.c19
-rw-r--r--src/xmpp/stanza.h3
-rw-r--r--src/xmpp/vcard.c1613
-rw-r--r--src/xmpp/vcard.h171
-rw-r--r--src/xmpp/vcard_funcs.h66
-rw-r--r--tests/unittests/ui/stub_ui.c5
-rw-r--r--tests/unittests/ui/stub_vcardwin.c4
-rw-r--r--tests/unittests/xmpp/stub_vcard.c79
25 files changed, 3688 insertions, 7 deletions
diff --git a/Makefile.am b/Makefile.am
index 2a906a8e..76b16d57 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -20,6 +20,7 @@ core_sources = \
 	src/xmpp/form.c src/xmpp/form.h \
 	src/xmpp/avatar.c src/xmpp/avatar.h \
 	src/xmpp/ox.c src/xmpp/ox.h \
+	src/xmpp/vcard.c src/xmpp/vcard.h src/xmpp/vcard_funcs.h \
 	src/event/common.c src/event/common.h \
 	src/event/server_events.c src/event/server_events.h \
 	src/event/client_events.c src/event/client_events.h \
@@ -37,6 +38,7 @@ core_sources = \
 	src/ui/privwin.c \
 	src/ui/confwin.c \
 	src/ui/xmlwin.c \
+	src/ui/vcardwin.c \
 	src/command/cmd_defs.h src/command/cmd_defs.c \
 	src/command/cmd_funcs.h src/command/cmd_funcs.c \
 	src/command/cmd_ac.h src/command/cmd_ac.c \
@@ -88,6 +90,7 @@ unittest_sources = \
 	src/omemo/omemo.h \
 	src/omemo/crypto.h \
 	src/omemo/store.h \
+	src/xmpp/vcard.h src/xmpp/vcard_funcs.h \
 	src/command/cmd_defs.h src/command/cmd_defs.c \
 	src/command/cmd_funcs.h src/command/cmd_funcs.c \
 	src/command/cmd_ac.h src/command/cmd_ac.c \
@@ -119,11 +122,13 @@ unittest_sources = \
 	src/event/server_events.c src/event/server_events.h \
 	src/event/client_events.c src/event/client_events.h \
 	src/ui/tray.h src/ui/tray.c \
+	tests/unittests/xmpp/stub_vcard.c \
 	tests/unittests/xmpp/stub_avatar.c \
 	tests/unittests/xmpp/stub_ox.c \
 	tests/unittests/xmpp/stub_xmpp.c \
 	tests/unittests/xmpp/stub_message.c \
 	tests/unittests/ui/stub_ui.c tests/unittests/ui/stub_ui.h \
+	tests/unittests/ui/stub_vcardwin.c \
 	tests/unittests/log/stub_log.c \
 	tests/unittests/chatlog/stub_chatlog.c \
 	tests/unittests/database/stub_database.c \
diff --git a/src/command/cmd_ac.c b/src/command/cmd_ac.c
index 93a503d1..9a839258 100644
--- a/src/command/cmd_ac.c
+++ b/src/command/cmd_ac.c
@@ -41,6 +41,7 @@
 #include <assert.h>
 #include <libgen.h>
 #include <dirent.h>
+#include <ctype.h>
 
 #include "common.h"
 #include "config/preferences.h"
@@ -131,6 +132,7 @@ static char* _lastactivity_autocomplete(ProfWin* window, const char* const input
 static char* _intype_autocomplete(ProfWin* window, const char* const input, gboolean previous);
 static char* _mood_autocomplete(ProfWin* window, const char* const input, gboolean previous);
 static char* _adhoc_cmd_autocomplete(ProfWin* window, const char* const input, gboolean previous);
+static char* _vcard_autocomplete(ProfWin* window, const char* const input, gboolean previous);
 
 static char* _script_autocomplete_func(const char* const prefix, gboolean previous, void* context);
 
@@ -276,6 +278,15 @@ static Autocomplete mood_ac;
 static Autocomplete mood_type_ac;
 static Autocomplete adhoc_cmd_ac;
 static Autocomplete lastactivity_ac;
+static Autocomplete vcard_ac;
+static Autocomplete vcard_photo_ac;
+static Autocomplete vcard_element_ac;
+static Autocomplete vcard_set_ac;
+static Autocomplete vcard_name_ac;
+static Autocomplete vcard_set_param_ac;
+static Autocomplete vcard_togglable_param_ac;
+static Autocomplete vcard_toggle_ac;
+static Autocomplete vcard_address_type_ac;
 
 /*!
  * \brief Initialization of auto completion for commands.
@@ -1181,6 +1192,89 @@ cmd_ac_init(void)
     lastactivity_ac = autocomplete_new();
     autocomplete_add(lastactivity_ac, "set");
     autocomplete_add(lastactivity_ac, "get");
+
+    vcard_ac = autocomplete_new();
+    autocomplete_add(vcard_ac, "get");
+    autocomplete_add(vcard_ac, "photo");
+    autocomplete_add(vcard_ac, "set");
+    autocomplete_add(vcard_ac, "add");
+    autocomplete_add(vcard_ac, "remove");
+    autocomplete_add(vcard_ac, "save");
+
+    vcard_photo_ac = autocomplete_new();
+    autocomplete_add(vcard_photo_ac, "open");
+    autocomplete_add(vcard_photo_ac, "save");
+
+    vcard_element_ac = autocomplete_new();
+    autocomplete_add(vcard_element_ac, "nickname");
+    autocomplete_add(vcard_element_ac, "birthday");
+    autocomplete_add(vcard_element_ac, "address");
+    autocomplete_add(vcard_element_ac, "tel");
+    autocomplete_add(vcard_element_ac, "email");
+    autocomplete_add(vcard_element_ac, "jid");
+    autocomplete_add(vcard_element_ac, "title");
+    autocomplete_add(vcard_element_ac, "role");
+    autocomplete_add(vcard_element_ac, "note");
+    autocomplete_add(vcard_element_ac, "url");
+
+    vcard_set_ac = autocomplete_new();
+    autocomplete_add(vcard_set_ac, "fullname");
+    autocomplete_add(vcard_set_ac, "name");
+
+    vcard_name_ac = autocomplete_new();
+    autocomplete_add(vcard_name_ac, "family");
+    autocomplete_add(vcard_name_ac, "given");
+    autocomplete_add(vcard_name_ac, "middle");
+    autocomplete_add(vcard_name_ac, "prefix");
+    autocomplete_add(vcard_name_ac, "suffix");
+
+    vcard_set_param_ac = autocomplete_new();
+    autocomplete_add(vcard_set_param_ac, "pobox");
+    autocomplete_add(vcard_set_param_ac, "extaddr");
+    autocomplete_add(vcard_set_param_ac, "street");
+    autocomplete_add(vcard_set_param_ac, "locality");
+    autocomplete_add(vcard_set_param_ac, "region");
+    autocomplete_add(vcard_set_param_ac, "pocode");
+    autocomplete_add(vcard_set_param_ac, "country");
+    autocomplete_add(vcard_set_param_ac, "type");
+    autocomplete_add(vcard_set_param_ac, "home");
+    autocomplete_add(vcard_set_param_ac, "work");
+    autocomplete_add(vcard_set_param_ac, "voice");
+    autocomplete_add(vcard_set_param_ac, "fax");
+    autocomplete_add(vcard_set_param_ac, "pager");
+    autocomplete_add(vcard_set_param_ac, "msg");
+    autocomplete_add(vcard_set_param_ac, "cell");
+    autocomplete_add(vcard_set_param_ac, "video");
+    autocomplete_add(vcard_set_param_ac, "bbs");
+    autocomplete_add(vcard_set_param_ac, "modem");
+    autocomplete_add(vcard_set_param_ac, "isdn");
+    autocomplete_add(vcard_set_param_ac, "pcs");
+    autocomplete_add(vcard_set_param_ac, "preferred");
+    autocomplete_add(vcard_set_param_ac, "x400");
+
+    vcard_togglable_param_ac = autocomplete_new();
+    autocomplete_add(vcard_togglable_param_ac, "home");
+    autocomplete_add(vcard_togglable_param_ac, "work");
+    autocomplete_add(vcard_togglable_param_ac, "voice");
+    autocomplete_add(vcard_togglable_param_ac, "fax");
+    autocomplete_add(vcard_togglable_param_ac, "pager");
+    autocomplete_add(vcard_togglable_param_ac, "msg");
+    autocomplete_add(vcard_togglable_param_ac, "cell");
+    autocomplete_add(vcard_togglable_param_ac, "video");
+    autocomplete_add(vcard_togglable_param_ac, "bbs");
+    autocomplete_add(vcard_togglable_param_ac, "modem");
+    autocomplete_add(vcard_togglable_param_ac, "isdn");
+    autocomplete_add(vcard_togglable_param_ac, "pcs");
+    autocomplete_add(vcard_togglable_param_ac, "preferred");
+    autocomplete_add(vcard_togglable_param_ac, "x400");
+
+    vcard_toggle_ac = autocomplete_new();
+    autocomplete_add(vcard_toggle_ac, "on");
+    autocomplete_add(vcard_toggle_ac, "off");
+
+    vcard_address_type_ac = autocomplete_new();
+    autocomplete_add(vcard_address_type_ac, "domestic");
+    autocomplete_add(vcard_address_type_ac, "international");
 }
 
 void
@@ -1502,6 +1596,17 @@ cmd_ac_reset(ProfWin* window)
     autocomplete_reset(mood_ac);
     autocomplete_reset(mood_type_ac);
     autocomplete_reset(adhoc_cmd_ac);
+
+    autocomplete_reset(vcard_ac);
+    autocomplete_reset(vcard_photo_ac);
+    autocomplete_reset(vcard_element_ac);
+    autocomplete_reset(vcard_set_ac);
+    autocomplete_reset(vcard_name_ac);
+    autocomplete_reset(vcard_set_param_ac);
+    autocomplete_reset(vcard_togglable_param_ac);
+    autocomplete_reset(vcard_toggle_ac);
+    autocomplete_reset(vcard_address_type_ac);
+
     autocomplete_reset(script_ac);
     autocomplete_reset(lastactivity_ac);
 
@@ -1672,6 +1777,15 @@ cmd_ac_uninit(void)
     autocomplete_free(intype_ac);
     autocomplete_free(adhoc_cmd_ac);
     autocomplete_free(lastactivity_ac);
+    autocomplete_free(vcard_ac);
+    autocomplete_free(vcard_photo_ac);
+    autocomplete_free(vcard_element_ac);
+    autocomplete_free(vcard_set_ac);
+    autocomplete_free(vcard_name_ac);
+    autocomplete_free(vcard_set_param_ac);
+    autocomplete_free(vcard_togglable_param_ac);
+    autocomplete_free(vcard_toggle_ac);
+    autocomplete_free(vcard_address_type_ac);
 }
 
 static void
@@ -1944,6 +2058,7 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ
     g_hash_table_insert(ac_funcs, "/intype", _intype_autocomplete);
     g_hash_table_insert(ac_funcs, "/mood", _mood_autocomplete);
     g_hash_table_insert(ac_funcs, "/cmd", _adhoc_cmd_autocomplete);
+    g_hash_table_insert(ac_funcs, "/vcard", _vcard_autocomplete);
 
     int len = strlen(input);
     char parsed[len + 1];
@@ -4322,3 +4437,138 @@ _adhoc_cmd_autocomplete(ProfWin* window, const char* const input, gboolean previ
 
     return result;
 }
+
+static char*
+_vcard_autocomplete(ProfWin* window, const char* const input, gboolean previous)
+{
+    char* result = NULL;
+
+    gboolean parse_result = FALSE;
+    gchar** args = parse_args(input, 0, 7, &parse_result);
+
+    if (parse_result && (g_strcmp0(args[0], "set") == 0)) {
+        gboolean space_at_end = g_str_has_suffix(input, " ");
+        int num_args = g_strv_length(args);
+        gboolean is_num = TRUE;
+
+        if (num_args >= 2) {
+            for (int i = 0; i < strlen(args[1]); i++) {
+                if (!isdigit((int)args[1][i])) {
+                    is_num = FALSE;
+                    break;
+                }
+            }
+        }
+
+        if ((num_args == 2 && space_at_end && is_num) || (num_args == 3 && !space_at_end && is_num)) {
+            GString* beginning = g_string_new("/vcard");
+            g_string_append_printf(beginning, " %s %s", args[0], args[1]);
+            result = autocomplete_param_with_ac(input, beginning->str, vcard_set_param_ac, TRUE, previous);
+            g_string_free(beginning, TRUE);
+            if (result) {
+                g_strfreev(args);
+                return result;
+            }
+        } else if ((num_args == 3 && space_at_end && is_num && (g_strcmp0(args[2], "type") == 0)) || (num_args == 4 && !space_at_end && is_num && (g_strcmp0(args[2], "type") == 0))) {
+            GString* beginning = g_string_new("/vcard");
+            g_string_append_printf(beginning, " %s %s %s", args[0], args[1], args[2]);
+            result = autocomplete_param_with_ac(input, beginning->str, vcard_address_type_ac, TRUE, previous);
+            g_string_free(beginning, TRUE);
+            if (result) {
+                g_strfreev(args);
+                return result;
+            }
+        } else if ((num_args == 3 && space_at_end && is_num && autocomplete_contains(vcard_togglable_param_ac, args[2])) || (num_args == 4 && !space_at_end && is_num && autocomplete_contains(vcard_togglable_param_ac, args[2]))) {
+            GString* beginning = g_string_new("/vcard");
+            g_string_append_printf(beginning, " %s %s %s", args[0], args[1], args[2]);
+            result = autocomplete_param_with_ac(input, beginning->str, vcard_toggle_ac, TRUE, previous);
+            g_string_free(beginning, TRUE);
+            if (result) {
+                g_strfreev(args);
+                return result;
+            }
+        } else {
+            result = autocomplete_param_with_ac(input, "/vcard set name", vcard_name_ac, TRUE, previous);
+
+            if (result) {
+                return result;
+            }
+
+            result = autocomplete_param_with_ac(input, "/vcard set", vcard_set_ac, TRUE, previous);
+
+            if (result) {
+                return result;
+            }
+        }
+    }
+
+    result = autocomplete_param_with_ac(input, "/vcard add", vcard_element_ac, TRUE, previous);
+
+    if (result) {
+        return result;
+    }
+
+    if (window->type == WIN_MUC) {
+        char* unquoted = strip_arg_quotes(input);
+
+        ProfMucWin* mucwin = (ProfMucWin*)window;
+        assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
+        Autocomplete nick_ac = muc_roster_ac(mucwin->roomjid);
+
+        if (nick_ac) {
+            result = autocomplete_param_with_ac(unquoted, "/vcard get", nick_ac, TRUE, previous);
+            if (result) {
+                free(unquoted);
+                return result;
+            }
+
+            result = autocomplete_param_with_ac(unquoted, "/vcard photo open", nick_ac, TRUE, previous);
+            if (result) {
+                free(unquoted);
+                return result;
+            }
+
+            result = autocomplete_param_with_ac(unquoted, "/vcard photo save", nick_ac, TRUE, previous);
+            if (result) {
+                free(unquoted);
+                return result;
+            }
+        }
+        free(unquoted);
+    } else {
+        char* unquoted = strip_arg_quotes(input);
+
+        result = autocomplete_param_with_func(unquoted, "/vcard get", roster_contact_autocomplete, previous, NULL);
+        if (result) {
+            free(unquoted);
+            return result;
+        }
+
+        result = autocomplete_param_with_func(unquoted, "/vcard photo open", roster_contact_autocomplete, previous, NULL);
+        if (result) {
+            free(unquoted);
+            return result;
+        }
+
+        result = autocomplete_param_with_func(unquoted, "/vcard photo save", roster_contact_autocomplete, previous, NULL);
+        if (result) {
+            free(unquoted);
+            return result;
+        }
+        free(unquoted);
+    }
+
+    result = autocomplete_param_with_ac(input, "/vcard photo", vcard_photo_ac, TRUE, previous);
+
+    if (result) {
+        return result;
+    }
+
+    result = autocomplete_param_with_ac(input, "/vcard", vcard_ac, TRUE, previous);
+
+    if (result) {
+        return result;
+    }
+
+    return result;
+}
diff --git a/src/command/cmd_defs.c b/src/command/cmd_defs.c
index 91346fce..4263ac22 100644
--- a/src/command/cmd_defs.c
+++ b/src/command/cmd_defs.c
@@ -1268,7 +1268,8 @@ static struct cmd_t command_defs[] = {
               "/time all|console|chat|muc|config|private|xml off",
               "/time statusbar set <format>",
               "/time statusbar off",
-              "/time lastactivity set <format>")
+              "/time lastactivity set <format>",
+              "/time vcard set <format>")
       CMD_DESC(
               "Configure time display preferences. "
               "Time formats are strings supported by g_date_time_format. "
@@ -1291,7 +1292,8 @@ static struct cmd_t command_defs[] = {
               { "statusbar set <format>", "Change time format in statusbar." },
               { "statusbar off", "Do not show time in status bar." },
               { "lastactivity set <format>", "Change time format for last activity." },
-              { "all set <format>", "Set time for: console, chat, muc, config, private and xml windows." },
+              { "vcard set <format>", "Change the time format used to display time/dates in vCard (such as birthdays)" },
+              { "all set <format>", "Set time for: console, chat, muc, config, private, and xml windows." },
               { "all off", "Do not show time for: console, chat, muc, config, private and xml windows." })
       CMD_EXAMPLES(
               "/time console set %H:%M:%S",
@@ -1582,6 +1584,124 @@ static struct cmd_t command_defs[] = {
               "/autoconnect off")
     },
 
+    { "/vcard",
+      parse_args, 0, 7, NULL,
+      CMD_SUBFUNCS(
+              {"add", cmd_vcard_add},
+              {"remove", cmd_vcard_remove},
+              {"get", cmd_vcard_get},
+              {"set", cmd_vcard_set},
+              {"photo", cmd_vcard_photo},
+              {"refresh", cmd_vcard_refresh},
+              {"save", cmd_vcard_save})
+      CMD_MAINFUNC(cmd_vcard)
+      CMD_TAGS(
+              CMD_TAG_CHAT,
+              CMD_TAG_GROUPCHAT)
+      CMD_SYN(
+              "/vcard get [<nick|contact>]",
+              "/vcard photo open <nick|contact> [<index>]",
+              "/vcard photo save <nick|contact> [output <filepath>] [index <index>]",
+              "/vcard set fullname <fullname>",
+              "/vcard set name family <family>",
+              "/vcard set name given <given>",
+              "/vcard set name middle <middle>",
+              "/vcard set name prefix <prefix>",
+              "/vcard set name suffix <suffix>",
+              "/vcard set <index> [<value>]",
+              "/vcard set <index> pobox <value>",
+              "/vcard set <index> extaddr <value>",
+              "/vcard set <index> street <value>",
+              "/vcard set <index> locality <value>",
+              "/vcard set <index> region <value>",
+              "/vcard set <index> pocode <value>",
+              "/vcard set <index> country <value>",
+              "/vcard set <index> type domestic|international",
+              "/vcard set <index> home on|off",
+              "/vcard set <index> work on|off",
+              "/vcard set <index> voice on|off",
+              "/vcard set <index> fax on|off",
+              "/vcard set <index> pager on|off",
+              "/vcard set <index> msg on|off",
+              "/vcard set <index> cell on|off",
+              "/vcard set <index> video on|off",
+              "/vcard set <index> bbs on|off",
+              "/vcard set <index> modem on|off",
+              "/vcard set <index> isdn on|off",
+              "/vcard set <index> pcs on|off",
+              "/vcard set <index> preferred on|off",
+              "/vcard set <index> parcel on|off",
+              "/vcard set <index> postal on|off",
+              "/vcard set <index> internet on|off",
+              "/vcard set <index> x400 on|off",
+              "/vcard add nickname <nickname>",
+              "/vcard add birthday <date>",
+              "/vcard add address",
+              "/vcard add tel <number>",
+              "/vcard add email <userid>",
+              "/vcard add jid <jid>",
+              "/vcard add title <title>",
+              "/vcard add role <role>",
+              "/vcard add note <note>",
+              "/vcard add url <url>",
+              "/vcard remove <index>",
+              "/vcard refresh",
+              "/vcard save")
+      CMD_DESC(
+              "Read your vCard or a user's vCard, get a user's avatar via their vCard, or modify your vCard. If no arguments are given, your vCard will be displayed in a new window, or an existing vCard window.")
+      CMD_ARGS(
+              { "get [<nick|contact>]", "Get your vCard, if a nickname/contact is provided, get that user's vCard" },
+              { "photo open <nick|contact> [<index>]", "Download a user's photo from their vCard to a file, and open it. If index is not specified, download the first photo (usually avatar) from their vCard" },
+              { "photo save <nick|contact>", "Download a user's photo from their vCard to a file. If index is not specified, download the first photo (usually avatar) from their vCard. If output is not specified, download the photo to profanity's photos directory." },
+              { "photo open-self [<index>]", "Download a photo from your vCard to a file, and open it. If index is not specified, download the first photo (usually avatar) from your vCard" },
+              { "photo save-self", "Download a photo from your vCard to a file. If index is not specified, download the first photo (usually avatar) from your vCard. If output is not specified, download the photo to profanity's photos directory. Same arguments as `photo open`" },
+              { "set fullname <fullname>", "Set your vCard's fullname to the specified value" },
+              { "set name family <family>", "Set your vCard's family name to the specified value" },
+              { "set name given <given>", "Set your vCard's given name to the specified value" },
+              { "set name middle <middle>", "Set your vCard's middle name to the specified value" },
+              { "set name prefix <prefix>", "Set your vCard's prefix name to the specified value" },
+              { "set name suffix <suffix>", "Set your vCard's suffix name to the specified value" },
+              { "set <index> [<value>]", "Set the main field in a element in your vCard to the specified value, or if no value was specified, modify the field in an editor, This only works in elements that have one field." },
+              { "set <index> pobox <value>", "Set the P.O. box in an address element in your vCard to the specified value." },
+              { "set <index> extaddr <value>", "Set the extended address in an address element in your vCard to the specified value." },
+              { "set <index> street <value>", "Set the street in an address element in your vCard to the specified value." },
+              { "set <index> locality <value>", "Set the locality in an address element in your vCard to the specified value." },
+              { "set <index> region <value>", "Set the region in an address element in your vCard to the specified value." },
+              { "set <index> pocode <value>", "Set the P.O. code in an address element in your vCard to the specified value." },
+              { "set <index> type domestic|international", "Set the type in an address element in your vCard to either domestic or international." },
+              { "set <index> home on|off", "Set the home option in an element in your vCard. (address, telephone, e-mail only)" },
+              { "set <index> work on|off", "Set the work option in an element in your vCard. (address, telephone, e-mail only)" },
+              { "set <index> voice on|off", "Set the voice option in a telephone element in your vCard." },
+              { "set <index> fax on|off", "Set the fax option in a telephone element in your vCard." },
+              { "set <index> pager on|off", "Set the pager option in a telephone element in your vCard." },
+              { "set <index> msg on|off", "Set the message option in a telephone element in your vCard." },
+              { "set <index> cell on|off", "Set the cellphone option in a telephone element in your vCard." },
+              { "set <index> video on|off", "Set the video option in a telephone element in your vCard." },
+              { "set <index> bbs on|off", "Set the BBS option in a telephone element in your vCard." },
+              { "set <index> modem on|off", "Set the modem option in a telephone element in your vCard." },
+              { "set <index> isdn on|off", "Set the ISDN option in a telephone element in your vCard." },
+              { "set <index> pcs on|off", "Set the PCS option in a telephone element in your vCard." },
+              { "set <index> preferred on|off", "Set the preferred option in an element in your vCard. (address, telephone, e-mail only)" },
+              { "set <index> parcel on|off", "Set the parcel option in an address element in your vCard." },
+              { "set <index> postal on|off", "Set the postal option in an address element in your vCard." },
+              { "set <index> internet on|off", "Set the internet option in an e-mail address in your vCard." },
+              { "set <index> x400 on|off", "Set the X400 option in an e-mail address in your vCard." },
+              { "add nickname <nickname>", "Add a nickname to your vCard" },
+              { "add birthday <date>", "Add a birthday date to your vCard" },
+              { "add address", "Add an address to your vCard" },
+              { "add tel <number>", "Add a telephone number to your vCard" },
+              { "add email <userid>", "Add an e-mail address to your vCard" },
+              { "add jid <jid>", "Add a Jabber ID to your vCard" },
+              { "add title <title>", "Add a title to your vCard" },
+              { "add role <role>", "Add a role to your vCard" },
+              { "add note <note>", "Add a note to your vCard" },
+              { "add url <url>", "Add a URL to your vCard" },
+              { "remove <index>", "Remove a element in your vCard by index" },
+              { "refresh", "Refreshes the local copy of the current account's vCard (undoes all your unpublished modifications)" },
+              { "save", "Save changes to the server" })
+      CMD_NOEXAMPLES
+    },
+
     { "/vercheck",
       parse_args, 0, 1, NULL,
       CMD_NOSUBFUNCS
@@ -2559,7 +2679,8 @@ static struct cmd_t command_defs[] = {
               { "avatar",  cmd_executable_avatar },
               { "urlopen", cmd_executable_urlopen },
               { "urlsave", cmd_executable_urlsave },
-              { "editor", cmd_executable_editor })
+              { "editor", cmd_executable_editor },
+              { "vcard_photo", cmd_executable_vcard_photo })
       CMD_NOMAINFUNC
       CMD_TAGS(
               CMD_TAG_DISCOVERY)
@@ -2568,7 +2689,9 @@ static struct cmd_t command_defs[] = {
               "/executable urlopen set <cmdtemplate>",
               "/executable urlopen default",
               "/executable urlsave set <cmdtemplate>",
-              "/executable urlsave default")
+              "/executable urlsave default",
+              "/executable vcard_photo set <cmdtemplate>",
+              "/executable vcard_photo default")
       CMD_DESC(
               "Configure executable that should be called upon a certain command.")
       CMD_ARGS(
@@ -2577,7 +2700,9 @@ static struct cmd_t command_defs[] = {
               { "urlopen default", "Restore to default settings." },
               { "urlsave set", "Set executable that is run by /url save. Takes a command template that replaces %u and %p with the URL and path respectively." },
               { "urlsave default", "Use the built-in download method for saving." },
-              { "editor set", "Set editor to be used with /editor. Needs a terminal editor or a script to run a graphical editor." })
+              { "editor set", "Set editor to be used with /editor. Needs a terminal editor or a script to run a graphical editor." },
+              { "vcard_photo set", "Set executable that is run by /vcard photo open. Takes a command template that replaces %p with the path" },
+              { "vcard_photo default", "Restore to default settings." })
       CMD_EXAMPLES(
               "/executable avatar xdg-open",
               "/executable urlopen set \"xdg-open %u\"",
@@ -2586,6 +2711,7 @@ static struct cmd_t command_defs[] = {
               "/executable urlsave set \"wget %u -O %p\"",
               "/executable urlsave set \"curl %u -o %p\"",
               "/executable urlsave default",
+              "/executable vcard_photo set \"feh %p\"",
               "/executable editor set vim")
     },
 
diff --git a/src/command/cmd_funcs.c b/src/command/cmd_funcs.c
index 9978e889..420bc0f6 100644
--- a/src/command/cmd_funcs.c
+++ b/src/command/cmd_funcs.c
@@ -91,6 +91,7 @@
 #include "xmpp/chat_session.h"
 #include "xmpp/avatar.h"
 #include "xmpp/stanza.h"
+#include "xmpp/vcard_funcs.h"
 
 #ifdef HAVE_LIBOTR
 #include "otr/otr.h"
@@ -5580,6 +5581,25 @@ cmd_time(ProfWin* window, const char* const command, gchar** args)
             cons_bad_cmd_usage(command);
             return TRUE;
         }
+    } else if (g_strcmp0(args[0], "vcard") == 0) {
+        if (args[1] == NULL) {
+            char* format = prefs_get_string(PREF_TIME_VCARD);
+            cons_show("vCard time format: %s", format);
+            g_free(format);
+            return TRUE;
+        } else if (g_strcmp0(args[1], "set") == 0 && args[2] != NULL) {
+            prefs_set_string(PREF_TIME_VCARD, args[2]);
+            cons_show("vCard time format set to '%s'.", args[2]);
+            ui_redraw();
+            return TRUE;
+        } else if (g_strcmp0(args[1], "off") == 0) {
+            cons_show("vCard time cannot be disabled.");
+            ui_redraw();
+            return TRUE;
+        } else {
+            cons_bad_cmd_usage(command);
+            return TRUE;
+        }
     } else {
         cons_bad_cmd_usage(command);
         return TRUE;
@@ -9639,6 +9659,23 @@ cmd_executable_editor(ProfWin* window, const char* const command, gchar** args)
 }
 
 gboolean
+cmd_executable_vcard_photo(ProfWin* window, const char* const command, gchar** args)
+{
+    if (g_strcmp0(args[1], "set") == 0 && args[2] != NULL) {
+        prefs_set_string(PREF_VCARD_PHOTO_CMD, args[2]);
+        cons_show("`vcard photo open` command set to invoke '%s'", args[2]);
+    } else if (g_strcmp0(args[1], "default") == 0) {
+        prefs_set_string(PREF_VCARD_PHOTO_CMD, NULL);
+        char* cmd = prefs_get_string(PREF_VCARD_PHOTO_CMD);
+        cons_show("`vcard photo open` command set to invoke '%s' (default)", cmd);
+        g_free(cmd);
+    } else {
+        cons_bad_cmd_usage(command);
+    }
+
+    return TRUE;
+}
+gboolean
 cmd_mam(ProfWin* window, const char* const command, gchar** args)
 {
     _cmd_set_boolean_preference(args[0], command, "Message Archive Management", PREF_MAM);
@@ -9818,3 +9855,956 @@ cmd_mood(ProfWin* window, const char* const command, gchar** args)
 
     return TRUE;
 }
+
+gboolean
+cmd_vcard(ProfWin* window, const char* const command, gchar** args)
+{
+    if (connection_get_status() != JABBER_CONNECTED) {
+        cons_show("You are not currently connected.");
+        return TRUE;
+    }
+
+    ProfVcardWin* vcardwin = wins_get_vcard();
+
+    if (vcardwin) {
+        ui_focus_win((ProfWin*)vcardwin);
+    } else {
+        vcardwin = (ProfVcardWin*)vcard_user_create_win();
+        ui_focus_win((ProfWin*)vcardwin);
+    }
+    vcardwin_update();
+    return TRUE;
+}
+
+gboolean
+cmd_vcard_add(ProfWin* window, const char* const command, gchar** args)
+{
+    if (connection_get_status() != JABBER_CONNECTED) {
+        cons_show("You are not currently connected.");
+        return TRUE;
+    }
+
+    vcard_element_t* element = calloc(1, sizeof(vcard_element_t));
+    if (!element) {
+        cons_show_error("Memory allocation failed.");
+        return TRUE;
+    }
+
+    struct tm tm;
+    gchar* type = args[1];
+    gchar* value = args[2];
+
+    if (g_strcmp0(type, "nickname") == 0) {
+        element->type = VCARD_NICKNAME;
+
+        element->nickname = strdup(value);
+    } else if (g_strcmp0(type, "birthday") == 0) {
+        element->type = VCARD_BIRTHDAY;
+
+        memset(&tm, 0, sizeof(struct tm));
+        if (!strptime(value, "%Y-%m-%d", &tm)) {
+            cons_show_error("Error parsing ISO8601 date.");
+            free(element);
+            return TRUE;
+        }
+        element->birthday = g_date_time_new_local(tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, 0, 0, 0);
+    } else if (g_strcmp0(type, "tel") == 0) {
+        element->type = VCARD_TELEPHONE;
+        if (value) {
+            element->telephone.number = strdup(value);
+        }
+    } else if (g_strcmp0(type, "address") == 0) {
+        element->type = VCARD_ADDRESS;
+    } else if (g_strcmp0(type, "email") == 0) {
+        element->type = VCARD_EMAIL;
+        if (value) {
+            element->email.userid = strdup(value);
+        }
+    } else if (g_strcmp0(type, "jid") == 0) {
+        element->type = VCARD_JID;
+        if (value) {
+            element->jid = strdup(value);
+        }
+    } else if (g_strcmp0(type, "title") == 0) {
+        element->type = VCARD_TITLE;
+        if (value) {
+            element->title = strdup(value);
+        }
+    } else if (g_strcmp0(type, "role") == 0) {
+        element->type = VCARD_ROLE;
+        if (value) {
+            element->role = strdup(value);
+        }
+    } else if (g_strcmp0(type, "note") == 0) {
+        element->type = VCARD_NOTE;
+        if (value) {
+            element->note = strdup(value);
+        }
+    } else if (g_strcmp0(type, "url") == 0) {
+        element->type = VCARD_URL;
+        if (value) {
+            element->url = strdup(value);
+        }
+    } else {
+        cons_bad_cmd_usage(command);
+        free(element);
+        return TRUE;
+    }
+
+    vcard_user_add_element(element);
+    vcardwin_update();
+    return TRUE;
+}
+
+gboolean
+cmd_vcard_remove(ProfWin* window, const char* const command, gchar** args)
+{
+    if (connection_get_status() != JABBER_CONNECTED) {
+        cons_show("You are not currently connected.");
+        return TRUE;
+    }
+
+    if (args[1]) {
+        vcard_user_remove_element(atoi(args[1]));
+        cons_show("Removed element at index %d", atoi(args[1]));
+        vcardwin_update();
+    } else {
+        cons_bad_cmd_usage(command);
+    }
+    return TRUE;
+}
+
+gboolean
+cmd_vcard_get(ProfWin* window, const char* const command, gchar** args)
+{
+    char* user = args[1];
+    xmpp_ctx_t* const ctx = connection_get_ctx();
+
+    if (connection_get_status() != JABBER_CONNECTED) {
+        cons_show("You are not currently connected.");
+        return TRUE;
+    }
+
+    if (user) {
+        // get the JID when in MUC window
+        if (window->type == WIN_MUC) {
+            ProfMucWin* mucwin = (ProfMucWin*)window;
+            assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
+
+            if (muc_anonymity_type(mucwin->roomjid) == MUC_ANONYMITY_TYPE_NONANONYMOUS) {
+                // non-anon muc: get the user's jid and send vcard request to them
+                Occupant* occupant = muc_roster_item(mucwin->roomjid, user);
+                Jid* jid_occupant = jid_create(occupant->jid);
+
+                vcard_print(ctx, window, jid_occupant->barejid);
+                jid_destroy(jid_occupant);
+            } else {
+                // anon muc: send the vcard request through the MUC's server
+                GString* full_jid = g_string_new(mucwin->roomjid);
+                g_string_append(full_jid, "/");
+                g_string_append(full_jid, user);
+
+                vcard_print(ctx, window, full_jid->str);
+
+                g_string_free(full_jid, TRUE);
+            }
+        } else {
+            char* jid = roster_barejid_from_name(user);
+            if (!jid) {
+                cons_bad_cmd_usage(command);
+                return TRUE;
+            }
+
+            vcard_print(ctx, window, jid);
+        }
+    } else {
+        if (window->type == WIN_CHAT) {
+            ProfChatWin* chatwin = (ProfChatWin*)window;
+            assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK);
+
+            vcard_print(ctx, window, chatwin->barejid);
+        } else {
+            vcard_print(ctx, window, NULL);
+        }
+    }
+
+    return TRUE;
+}
+
+gboolean
+cmd_vcard_photo(ProfWin* window, const char* const command, gchar** args)
+{
+    char* operation = args[1];
+    char* user = args[2];
+
+    xmpp_ctx_t* const ctx = connection_get_ctx();
+
+    if (connection_get_status() != JABBER_CONNECTED) {
+        cons_show("You are not currently connected.");
+        return TRUE;
+    }
+
+    gboolean jidless = (g_strcmp0(operation, "open-self") == 0 || g_strcmp0(operation, "save-self") == 0);
+
+    if (!operation || (!jidless && !user)) {
+        cons_bad_cmd_usage(command);
+        return TRUE;
+    }
+
+    char* jid = NULL;
+    char* filepath = NULL;
+    int index = 0;
+
+    if (!jidless) {
+        if (window->type == WIN_MUC) {
+            ProfMucWin* mucwin = (ProfMucWin*)window;
+            assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
+
+            if (muc_anonymity_type(mucwin->roomjid) == MUC_ANONYMITY_TYPE_NONANONYMOUS) {
+                // non-anon muc: get the user's jid and send vcard request to them
+                Occupant* occupant = muc_roster_item(mucwin->roomjid, user);
+                Jid* jid_occupant = jid_create(occupant->jid);
+
+                jid = g_strdup(jid_occupant->barejid);
+                jid_destroy(jid_occupant);
+            } else {
+                // anon muc: send the vcard request through the MUC's server
+                GString* full_jid = g_string_new(mucwin->roomjid);
+                g_string_append(full_jid, "/");
+                g_string_append(full_jid, user);
+
+                jid = full_jid->str;
+
+                g_string_free(full_jid, FALSE);
+            }
+        } else {
+            char* jid_temp = roster_barejid_from_name(user);
+            if (!jid_temp) {
+                cons_bad_cmd_usage(command);
+                return TRUE;
+            } else {
+                jid = g_strdup(jid_temp);
+            }
+        }
+    }
+    if (!g_strcmp0(operation, "open")) {
+        // if an index is provided
+        if (args[3]) {
+            vcard_photo(ctx, jid, NULL, atoi(args[3]), TRUE);
+        } else {
+            vcard_photo(ctx, jid, NULL, -1, TRUE);
+        }
+    } else if (!g_strcmp0(operation, "save")) {
+        // arguments
+        if (g_strv_length(args) > 2) {
+            gchar* opt_keys[] = { "output", "index", NULL };
+            gboolean parsed;
+
+            GHashTable* options = parse_options(&args[3], opt_keys, &parsed);
+            if (!parsed) {
+                cons_bad_cmd_usage(command);
+                options_destroy(options);
+                return TRUE;
+            }
+
+            filepath = g_hash_table_lookup(options, "output");
+            if (!filepath) {
+                filepath = NULL;
+            }
+
+            char* index_str = g_hash_table_lookup(options, "index");
+            if (!index_str) {
+                index = -1;
+            } else {
+                index = atoi(index_str);
+            }
+
+            options_destroy(options);
+        } else {
+            filepath = NULL;
+            index = -1;
+        }
+
+        vcard_photo(ctx, jid, filepath, index, FALSE);
+    } else if (!g_strcmp0(operation, "open-self")) {
+        // if an index is provided
+        if (args[2]) {
+            vcard_photo(ctx, NULL, NULL, atoi(args[2]), TRUE);
+        } else {
+            vcard_photo(ctx, NULL, NULL, -1, TRUE);
+        }
+    } else if (!g_strcmp0(operation, "save-self")) {
+        // arguments
+        if (g_strv_length(args) > 2) {
+            gchar* opt_keys[] = { "output", "index", NULL };
+            gboolean parsed;
+
+            GHashTable* options = parse_options(&args[2], opt_keys, &parsed);
+            if (!parsed) {
+                cons_bad_cmd_usage(command);
+                options_destroy(options);
+                return TRUE;
+            }
+
+            filepath = g_hash_table_lookup(options, "output");
+            if (!filepath) {
+                filepath = NULL;
+            }
+
+            char* index_str = g_hash_table_lookup(options, "index");
+            if (!index_str) {
+                index = -1;
+            } else {
+                index = atoi(index_str);
+            }
+
+            options_destroy(options);
+        } else {
+            filepath = NULL;
+            index = -1;
+        }
+
+        vcard_photo(ctx, NULL, filepath, index, FALSE);
+    } else {
+        cons_bad_cmd_usage(command);
+    }
+
+    if (!jidless) {
+        g_free(jid);
+    }
+    return TRUE;
+}
+
+gboolean
+cmd_vcard_refresh(ProfWin* window, const char* const command, gchar** args)
+{
+    if (connection_get_status() != JABBER_CONNECTED) {
+        cons_show("You are not currently connected.");
+        return TRUE;
+    }
+
+    vcard_user_refresh();
+    vcardwin_update();
+    return TRUE;
+}
+
+gboolean
+cmd_vcard_set(ProfWin* window, const char* const command, gchar** args)
+{
+    char* key = args[1];
+    char* value = args[2];
+
+    if (connection_get_status() != JABBER_CONNECTED) {
+        cons_show("You are not currently connected.");
+        return TRUE;
+    }
+
+    if (!key) {
+        cons_bad_cmd_usage(command);
+        return TRUE;
+    }
+
+    gboolean is_num = TRUE;
+    for (int i = 0; i < strlen(key); i++) {
+        if (!isdigit((int)key[i])) {
+            is_num = FALSE;
+            break;
+        }
+    }
+
+    if (g_strcmp0(key, "fullname") == 0 && value) {
+        vcard_user_set_fullname(value);
+        cons_show("User vCard's full name has been set");
+    } else if (g_strcmp0(key, "name") == 0 && value) {
+        char* value2 = args[3];
+
+        if (!value2) {
+            cons_bad_cmd_usage(command);
+            return TRUE;
+        }
+
+        if (g_strcmp0(value, "family") == 0) {
+            vcard_user_set_name_family(value2);
+            cons_show("User vCard's family name has been set");
+        } else if (g_strcmp0(value, "given") == 0) {
+            vcard_user_set_name_given(value2);
+            cons_show("User vCard's given name has been set");
+        } else if (g_strcmp0(value, "middle") == 0) {
+            vcard_user_set_name_middle(value2);
+            cons_show("User vCard's middle name has been set");
+        } else if (g_strcmp0(value, "prefix") == 0) {
+            vcard_user_set_name_prefix(value2);
+            cons_show("User vCard's prefix name has been set");
+        } else if (g_strcmp0(value, "suffix") == 0) {
+            vcard_user_set_name_suffix(value2);
+            cons_show("User vCard's suffix name has been set");
+        }
+    } else if (is_num) {
+        char* value2 = args[3];
+        struct tm tm;
+
+        vcard_element_t* element = vcard_user_get_element_index(atoi(key));
+
+        if (!element) {
+            cons_bad_cmd_usage(command);
+            return TRUE;
+        }
+
+        if (!value2 || !value) {
+            // Set the main field of element at index <key> to <value>, or from an editor
+
+            switch (element->type) {
+            case VCARD_NICKNAME:
+                if (!value) {
+                    gchar* editor_value;
+                    if (get_message_from_editor(element->nickname, &editor_value)) {
+                        return TRUE;
+                    }
+
+                    if (element->nickname) {
+                        free(element->nickname);
+                    }
+                    element->nickname = editor_value;
+                } else {
+                    if (element->nickname) {
+                        free(element->nickname);
+                    }
+                    element->nickname = strdup(value);
+                }
+                break;
+            case VCARD_BIRTHDAY:
+                memset(&tm, 0, sizeof(struct tm));
+                if (!strptime(value, "%Y-%m-%d", &tm)) {
+                    cons_show_error("Error parsing ISO8601 date.");
+                    return TRUE;
+                }
+
+                if (element->birthday) {
+                    g_date_time_unref(element->birthday);
+                }
+                element->birthday = g_date_time_new_local(tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, 0, 0, 0);
+                break;
+            case VCARD_TELEPHONE:
+                if (!value) {
+                    gchar* editor_value;
+                    if (get_message_from_editor(element->telephone.number, &editor_value)) {
+                        return TRUE;
+                    }
+
+                    if (element->telephone.number) {
+                        free(element->telephone.number);
+                    }
+                    element->telephone.number = editor_value;
+                } else {
+                    if (element->telephone.number) {
+                        free(element->telephone.number);
+                    }
+                    element->telephone.number = strdup(value);
+                }
+
+                break;
+            case VCARD_EMAIL:
+                if (!value) {
+                    gchar* editor_value;
+                    if (get_message_from_editor(element->email.userid, &editor_value)) {
+                        return TRUE;
+                    }
+
+                    if (element->email.userid) {
+                        free(element->email.userid);
+                    }
+                    element->email.userid = editor_value;
+                } else {
+                    if (element->email.userid) {
+                        free(element->email.userid);
+                    }
+                    element->email.userid = strdup(value);
+                }
+                break;
+            case VCARD_JID:
+                if (!value) {
+                    gchar* editor_value;
+                    if (get_message_from_editor(element->jid, &editor_value)) {
+                        return TRUE;
+                    }
+
+                    if (element->jid) {
+                        free(element->jid);
+                    }
+                    element->jid = editor_value;
+                } else {
+                    if (element->jid) {
+                        free(element->jid);
+                    }
+                    element->jid = strdup(value);
+                }
+                break;
+            case VCARD_TITLE:
+                if (!value) {
+                    gchar* editor_value;
+                    if (get_message_from_editor(element->title, &editor_value)) {
+                        return TRUE;
+                    }
+
+                    if (element->title) {
+                        free(element->title);
+                    }
+                    element->title = editor_value;
+                } else {
+                    if (element->title) {
+                        free(element->title);
+                    }
+                    element->title = strdup(value);
+                }
+                break;
+            case VCARD_ROLE:
+                if (!value) {
+                    gchar* editor_value;
+                    if (get_message_from_editor(element->role, &editor_value)) {
+                        return TRUE;
+                    }
+
+                    if (element->role) {
+                        free(element->role);
+                    }
+                    element->role = editor_value;
+                } else {
+                    if (element->role) {
+                        free(element->role);
+                    }
+                    element->role = strdup(value);
+                }
+                break;
+            case VCARD_NOTE:
+                if (!value) {
+                    gchar* editor_value;
+                    if (get_message_from_editor(element->note, &editor_value)) {
+                        return TRUE;
+                    }
+
+                    if (element->note) {
+                        free(element->note);
+                    }
+                    element->note = editor_value;
+                } else {
+                    if (element->note) {
+                        free(element->note);
+                    }
+                    element->note = strdup(value);
+                }
+                break;
+            case VCARD_URL:
+                if (!value) {
+                    gchar* editor_value;
+                    if (get_message_from_editor(element->url, &editor_value)) {
+                        return TRUE;
+                    }
+
+                    if (element->url) {
+                        free(element->url);
+                    }
+                    element->url = editor_value;
+                } else {
+                    if (element->url) {
+                        free(element->url);
+                    }
+                    element->url = strdup(value);
+                }
+                break;
+            default:
+                cons_show_error("Element unsupported");
+            }
+        } else if (value) {
+            if (g_strcmp0(value, "pobox") == 0 && element->type == VCARD_ADDRESS) {
+                if (!value2) {
+                    gchar* editor_value;
+                    if (get_message_from_editor(element->address.pobox, &editor_value)) {
+                        return TRUE;
+                    }
+
+                    if (element->address.pobox) {
+                        free(element->address.pobox);
+                    }
+                    element->address.pobox = editor_value;
+                } else {
+                    if (element->address.pobox) {
+                        free(element->address.pobox);
+                    }
+                    element->address.pobox = strdup(value2);
+                }
+            } else if (g_strcmp0(value, "extaddr") == 0 && element->type == VCARD_ADDRESS) {
+                if (!value2) {
+                    gchar* editor_value;
+                    if (get_message_from_editor(element->address.extaddr, &editor_value)) {
+                        return TRUE;
+                    }
+
+                    if (element->address.extaddr) {
+                        free(element->address.extaddr);
+                    }
+                    element->address.extaddr = editor_value;
+                } else {
+                    if (element->address.extaddr) {
+                        free(element->address.extaddr);
+                    }
+                    element->address.extaddr = strdup(value2);
+                }
+            } else if (g_strcmp0(value, "street") == 0 && element->type == VCARD_ADDRESS) {
+                if (!value2) {
+                    gchar* editor_value;
+                    if (get_message_from_editor(element->address.street, &editor_value)) {
+                        return TRUE;
+                    }
+
+                    if (element->address.street) {
+                        free(element->address.street);
+                    }
+                    element->address.street = editor_value;
+                } else {
+                    if (element->address.street) {
+                        free(element->address.street);
+                    }
+                    element->address.street = strdup(value2);
+                }
+            } else if (g_strcmp0(value, "locality") == 0 && element->type == VCARD_ADDRESS) {
+                if (!value2) {
+                    gchar* editor_value;
+                    if (get_message_from_editor(element->address.locality, &editor_value)) {
+                        return TRUE;
+                    }
+
+                    if (element->address.locality) {
+                        free(element->address.locality);
+                    }
+                    element->address.locality = editor_value;
+                } else {
+                    if (element->address.locality) {
+                        free(element->address.locality);
+                    }
+                    element->address.locality = strdup(value2);
+                }
+            } else if (g_strcmp0(value, "region") == 0 && element->type == VCARD_ADDRESS) {
+                if (!value2) {
+                    gchar* editor_value;
+                    if (get_message_from_editor(element->address.region, &editor_value)) {
+                        return TRUE;
+                    }
+
+                    if (element->address.region) {
+                        free(element->address.region);
+                    }
+                    element->address.region = editor_value;
+                } else {
+                    if (element->address.region) {
+                        free(element->address.region);
+                    }
+                    element->address.region = strdup(value2);
+                }
+            } else if (g_strcmp0(value, "pocode") == 0 && element->type == VCARD_ADDRESS) {
+                if (!value2) {
+                    gchar* editor_value;
+                    if (get_message_from_editor(element->address.pcode, &editor_value)) {
+                        return TRUE;
+                    }
+
+                    if (element->address.pcode) {
+                        free(element->address.pcode);
+                    }
+                    element->address.pcode = editor_value;
+                } else {
+                    if (element->address.pcode) {
+                        free(element->address.pcode);
+                    }
+                    element->address.pcode = strdup(value2);
+                }
+            } else if (g_strcmp0(value, "country") == 0 && element->type == VCARD_ADDRESS) {
+                if (!value2) {
+                    gchar* editor_value;
+                    if (get_message_from_editor(element->address.country, &editor_value)) {
+                        return TRUE;
+                    }
+
+                    if (element->address.country) {
+                        free(element->address.country);
+                    }
+                    element->address.country = editor_value;
+                } else {
+                    if (element->address.country) {
+                        free(element->address.country);
+                    }
+                    element->address.country = strdup(value2);
+                }
+            } else if (g_strcmp0(value, "type") == 0 && element->type == VCARD_ADDRESS) {
+                if (g_strcmp0(value2, "domestic") == 0) {
+                    element->address.options &= ~VCARD_INTL;
+                    element->address.options |= VCARD_DOM;
+                } else if (g_strcmp0(value2, "international") == 0) {
+                    element->address.options &= ~VCARD_DOM;
+                    element->address.options |= VCARD_INTL;
+                } else {
+                    cons_bad_cmd_usage(command);
+                    return TRUE;
+                }
+            } else if (g_strcmp0(value, "home") == 0) {
+                switch (element->type) {
+                case VCARD_ADDRESS:
+                    if (g_strcmp0(value2, "on") == 0) {
+                        element->address.options |= VCARD_HOME;
+                    } else if (g_strcmp0(value2, "off") == 0) {
+                        element->address.options &= ~VCARD_HOME;
+                    } else {
+                        cons_bad_cmd_usage(command);
+                        return TRUE;
+                    }
+                    break;
+                case VCARD_TELEPHONE:
+                    if (g_strcmp0(value2, "on") == 0) {
+                        element->telephone.options |= VCARD_HOME;
+                    } else if (g_strcmp0(value2, "off") == 0) {
+                        element->telephone.options &= ~VCARD_HOME;
+                    } else {
+                        cons_bad_cmd_usage(command);
+                        return TRUE;
+                    }
+                    break;
+                case VCARD_EMAIL:
+                    if (g_strcmp0(value2, "on") == 0) {
+                        element->email.options |= VCARD_HOME;
+                    } else if (g_strcmp0(value2, "off") == 0) {
+                        element->email.options &= ~VCARD_HOME;
+                    } else {
+                        cons_bad_cmd_usage(command);
+                        return TRUE;
+                    }
+                    break;
+                default:
+                    cons_bad_cmd_usage(command);
+                    return TRUE;
+                }
+            } else if (g_strcmp0(value, "work") == 0) {
+                switch (element->type) {
+                case VCARD_ADDRESS:
+                    if (g_strcmp0(value2, "on") == 0) {
+                        element->address.options |= VCARD_WORK;
+                    } else if (g_strcmp0(value2, "off") == 0) {
+                        element->address.options &= ~VCARD_WORK;
+                    } else {
+                        cons_bad_cmd_usage(command);
+                        return TRUE;
+                    }
+                    break;
+                case VCARD_TELEPHONE:
+                    if (g_strcmp0(value2, "on") == 0) {
+                        element->telephone.options |= VCARD_WORK;
+                    } else if (g_strcmp0(value2, "off") == 0) {
+                        element->telephone.options &= ~VCARD_WORK;
+                    } else {
+                        cons_bad_cmd_usage(command);
+                        return TRUE;
+                    }
+                    break;
+                case VCARD_EMAIL:
+                    if (g_strcmp0(value2, "on") == 0) {
+                        element->email.options |= VCARD_WORK;
+                    } else if (g_strcmp0(value2, "off") == 0) {
+                        element->email.options &= ~VCARD_WORK;
+                    } else {
+                        cons_bad_cmd_usage(command);
+                        return TRUE;
+                    }
+                    break;
+                default:
+                    cons_bad_cmd_usage(command);
+                    return TRUE;
+                }
+            } else if (g_strcmp0(value, "voice") == 0 && element->type == VCARD_TELEPHONE) {
+                if (g_strcmp0(value2, "on") == 0) {
+                    element->telephone.options |= VCARD_TEL_VOICE;
+                } else if (g_strcmp0(value2, "off") == 0) {
+                    element->telephone.options &= ~VCARD_TEL_VOICE;
+                } else {
+                    cons_bad_cmd_usage(command);
+                    return TRUE;
+                }
+            } else if (g_strcmp0(value, "fax") == 0 && element->type == VCARD_TELEPHONE) {
+                if (g_strcmp0(value2, "on") == 0) {
+                    element->telephone.options |= VCARD_TEL_FAX;
+                } else if (g_strcmp0(value2, "off") == 0) {
+                    element->telephone.options &= ~VCARD_TEL_FAX;
+                } else {
+                    cons_bad_cmd_usage(command);
+                    return TRUE;
+                }
+            } else if (g_strcmp0(value, "pager") == 0 && element->type == VCARD_TELEPHONE) {
+                if (g_strcmp0(value2, "on") == 0) {
+                    element->telephone.options |= VCARD_TEL_PAGER;
+                } else if (g_strcmp0(value2, "off") == 0) {
+                    element->telephone.options &= ~VCARD_TEL_PAGER;
+                } else {
+                    cons_bad_cmd_usage(command);
+                    return TRUE;
+                }
+            } else if (g_strcmp0(value, "msg") == 0 && element->type == VCARD_TELEPHONE) {
+                if (g_strcmp0(value2, "on") == 0) {
+                    element->telephone.options |= VCARD_TEL_MSG;
+                } else if (g_strcmp0(value2, "off") == 0) {
+                    element->telephone.options &= ~VCARD_TEL_MSG;
+                } else {
+                    cons_bad_cmd_usage(command);
+                    return TRUE;
+                }
+            } else if (g_strcmp0(value, "cell") == 0 && element->type == VCARD_TELEPHONE) {
+                if (g_strcmp0(value2, "on") == 0) {
+                    element->telephone.options |= VCARD_TEL_CELL;
+                } else if (g_strcmp0(value2, "off") == 0) {
+                    element->telephone.options &= ~VCARD_TEL_CELL;
+                } else {
+                    cons_bad_cmd_usage(command);
+                    return TRUE;
+                }
+            } else if (g_strcmp0(value, "video") == 0 && element->type == VCARD_TELEPHONE) {
+                if (g_strcmp0(value2, "on") == 0) {
+                    element->telephone.options |= VCARD_TEL_VIDEO;
+                } else if (g_strcmp0(value2, "off") == 0) {
+                    element->telephone.options &= ~VCARD_TEL_VIDEO;
+                } else {
+                    cons_bad_cmd_usage(command);
+                    return TRUE;
+                }
+            } else if (g_strcmp0(value, "bbs") == 0 && element->type == VCARD_TELEPHONE) {
+                if (g_strcmp0(value2, "on") == 0) {
+                    element->telephone.options |= VCARD_TEL_BBS;
+                } else if (g_strcmp0(value2, "off") == 0) {
+                    element->telephone.options &= ~VCARD_TEL_BBS;
+                } else {
+                    cons_bad_cmd_usage(command);
+                    return TRUE;
+                }
+            } else if (g_strcmp0(value, "modem") == 0 && element->type == VCARD_TELEPHONE) {
+                if (g_strcmp0(value2, "on") == 0) {
+                    element->telephone.options |= VCARD_TEL_MODEM;
+                } else if (g_strcmp0(value2, "off") == 0) {
+                    element->telephone.options &= ~VCARD_TEL_MODEM;
+                } else {
+                    cons_bad_cmd_usage(command);
+                    return TRUE;
+                }
+            } else if (g_strcmp0(value, "isdn") == 0 && element->type == VCARD_TELEPHONE) {
+                if (g_strcmp0(value2, "on") == 0) {
+                    element->telephone.options |= VCARD_TEL_ISDN;
+                } else if (g_strcmp0(value2, "off") == 0) {
+                    element->telephone.options &= ~VCARD_TEL_ISDN;
+                } else {
+                    cons_bad_cmd_usage(command);
+                    return TRUE;
+                }
+            } else if (g_strcmp0(value, "pcs") == 0 && element->type == VCARD_TELEPHONE) {
+                if (g_strcmp0(value2, "on") == 0) {
+                    element->telephone.options |= VCARD_TEL_PCS;
+                } else if (g_strcmp0(value2, "off") == 0) {
+                    element->telephone.options &= ~VCARD_TEL_PCS;
+                } else {
+                    cons_bad_cmd_usage(command);
+                    return TRUE;
+                }
+            } else if (g_strcmp0(value, "preferred") == 0) {
+                switch (element->type) {
+                case VCARD_ADDRESS:
+                    if (g_strcmp0(value2, "on") == 0) {
+                        element->address.options |= VCARD_PREF;
+                    } else if (g_strcmp0(value2, "off") == 0) {
+                        element->address.options &= ~VCARD_PREF;
+                    } else {
+                        cons_bad_cmd_usage(command);
+                        return TRUE;
+                    }
+                    break;
+                case VCARD_TELEPHONE:
+                    if (g_strcmp0(value2, "on") == 0) {
+                        element->telephone.options |= VCARD_PREF;
+                    } else if (g_strcmp0(value2, "off") == 0) {
+                        element->telephone.options &= ~VCARD_PREF;
+                    } else {
+                        cons_bad_cmd_usage(command);
+                        return TRUE;
+                    }
+                    break;
+                case VCARD_EMAIL:
+                    if (g_strcmp0(value2, "on") == 0) {
+                        element->email.options |= VCARD_PREF;
+                    } else if (g_strcmp0(value2, "off") == 0) {
+                        element->email.options &= ~VCARD_PREF;
+                    } else {
+                        cons_bad_cmd_usage(command);
+                        return TRUE;
+                    }
+                    break;
+                default:
+                    cons_bad_cmd_usage(command);
+                    return TRUE;
+                }
+            } else if (g_strcmp0(value, "parcel") == 0 && element->type == VCARD_ADDRESS) {
+                if (g_strcmp0(value2, "on") == 0) {
+                    element->address.options |= VCARD_PARCEL;
+                } else if (g_strcmp0(value2, "off") == 0) {
+                    element->address.options &= ~VCARD_PARCEL;
+                } else {
+                    cons_bad_cmd_usage(command);
+                    return TRUE;
+                }
+            } else if (g_strcmp0(value, "postal") == 0 && element->type == VCARD_ADDRESS) {
+                if (g_strcmp0(value2, "on") == 0) {
+                    element->address.options |= VCARD_POSTAL;
+                } else if (g_strcmp0(value2, "off") == 0) {
+                    element->address.options &= ~VCARD_POSTAL;
+                } else {
+                    cons_bad_cmd_usage(command);
+                    return TRUE;
+                }
+            } else if (g_strcmp0(value, "internet") == 0 && element->type == VCARD_EMAIL) {
+                if (g_strcmp0(value2, "on") == 0) {
+                    element->email.options |= VCARD_EMAIL_INTERNET;
+                } else if (g_strcmp0(value2, "off") == 0) {
+                    element->email.options &= ~VCARD_EMAIL_INTERNET;
+                } else {
+                    cons_bad_cmd_usage(command);
+                    return TRUE;
+                }
+            } else if (g_strcmp0(value, "x400") == 0 && element->type == VCARD_EMAIL) {
+                if (g_strcmp0(value2, "on") == 0) {
+                    element->email.options |= VCARD_EMAIL_X400;
+                } else if (g_strcmp0(value2, "off") == 0) {
+                    element->email.options &= ~VCARD_EMAIL_X400;
+                } else {
+                    cons_bad_cmd_usage(command);
+                    return TRUE;
+                }
+            } else {
+                cons_bad_cmd_usage(command);
+                return TRUE;
+            }
+        } else {
+            cons_bad_cmd_usage(command);
+            return TRUE;
+        }
+    } else {
+        cons_bad_cmd_usage(command);
+        return TRUE;
+    }
+
+    vcardwin_update();
+    return TRUE;
+}
+
+gboolean
+cmd_vcard_save(ProfWin* window, const char* const command, gchar** args)
+{
+    if (connection_get_status() != JABBER_CONNECTED) {
+        cons_show("You are not currently connected.");
+        return TRUE;
+    }
+
+    vcard_user_save();
+    cons_show("User vCard uploaded");
+    return TRUE;
+}
diff --git a/src/command/cmd_funcs.h b/src/command/cmd_funcs.h
index adc6793d..83594bdd 100644
--- a/src/command/cmd_funcs.h
+++ b/src/command/cmd_funcs.h
@@ -246,6 +246,7 @@ gboolean cmd_executable_avatar(ProfWin* window, const char* const command, gchar
 gboolean cmd_executable_urlopen(ProfWin* window, const char* const command, gchar** args);
 gboolean cmd_executable_urlsave(ProfWin* window, const char* const command, gchar** args);
 gboolean cmd_executable_editor(ProfWin* window, const char* const command, gchar** args);
+gboolean cmd_executable_vcard_photo(ProfWin* window, const char* const command, gchar** args);
 gboolean cmd_mam(ProfWin* window, const char* const command, gchar** args);
 gboolean cmd_editor(ProfWin* window, const char* const command, gchar** args);
 gboolean cmd_correct_editor(ProfWin* window, const char* const command, gchar** args);
@@ -253,5 +254,13 @@ gboolean cmd_silence(ProfWin* window, const char* const command, gchar** args);
 gboolean cmd_register(ProfWin* window, const char* const command, gchar** args);
 gboolean cmd_mood(ProfWin* window, const char* const command, gchar** args);
 gboolean cmd_stamp(ProfWin* window, const char* const command, gchar** args);
+gboolean cmd_vcard(ProfWin* window, const char* const command, gchar** args);
+gboolean cmd_vcard_add(ProfWin* window, const char* const command, gchar** args);
+gboolean cmd_vcard_remove(ProfWin* window, const char* const command, gchar** args);
+gboolean cmd_vcard_get(ProfWin* window, const char* const command, gchar** args);
+gboolean cmd_vcard_photo(ProfWin* window, const char* const command, gchar** args);
+gboolean cmd_vcard_refresh(ProfWin* window, const char* const command, gchar** args);
+gboolean cmd_vcard_set(ProfWin* window, const char* const command, gchar** args);
+gboolean cmd_vcard_save(ProfWin* window, const char* const command, gchar** args);
 
 #endif
diff --git a/src/config/files.h b/src/config/files.h
index 16739291..1fcee58e 100644
--- a/src/config/files.h
+++ b/src/config/files.h
@@ -60,6 +60,7 @@
 #define DIR_DOWNLOADS "downloads"
 #define DIR_EDITOR    "editor"
 #define DIR_CERTS     "certs"
+#define DIR_PHOTOS    "photos"
 
 void files_create_directories(void);
 
diff --git a/src/config/preferences.c b/src/config/preferences.c
index dec92e7f..122c36c6 100644
--- a/src/config/preferences.c
+++ b/src/config/preferences.c
@@ -1770,6 +1770,7 @@ _get_group(preference_t pref)
     case PREF_TIME_XMLCONSOLE:
     case PREF_TIME_STATUSBAR:
     case PREF_TIME_LASTACTIVITY:
+    case PREF_TIME_VCARD:
     case PREF_ROSTER:
     case PREF_ROSTER_OFFLINE:
     case PREF_ROSTER_RESOURCE:
@@ -1850,6 +1851,7 @@ _get_group(preference_t pref)
     case PREF_AVATAR_CMD:
     case PREF_URL_OPEN_CMD:
     case PREF_URL_SAVE_CMD:
+    case PREF_VCARD_PHOTO_CMD:
         return PREF_GROUP_EXECUTABLES;
     case PREF_AUTOAWAY_CHECK:
     case PREF_AUTOAWAY_MODE:
@@ -2029,6 +2031,8 @@ _get_key(preference_t pref)
         return "time.statusbar";
     case PREF_TIME_LASTACTIVITY:
         return "time.lastactivity";
+    case PREF_TIME_VCARD:
+        return "time.vcard";
     case PREF_ROSTER:
         return "roster";
     case PREF_ROSTER_OFFLINE:
@@ -2159,6 +2163,8 @@ _get_key(preference_t pref)
         return "log";
     case PREF_MOOD:
         return "mood";
+    case PREF_VCARD_PHOTO_CMD:
+        return "vcard.photo.cmd";
     default:
         return NULL;
     }
@@ -2269,6 +2275,8 @@ _get_default_string(preference_t pref)
         return "%H:%M";
     case PREF_TIME_LASTACTIVITY:
         return "%d/%m/%y %H:%M:%S";
+    case PREF_TIME_VCARD:
+        return "%d/%m/%y";
     case PREF_PGP_LOG:
         return "on";
     case PREF_CONSOLE_MUC:
@@ -2293,6 +2301,8 @@ _get_default_string(preference_t pref)
         return "xdg-open";
     case PREF_URL_OPEN_CMD:
         return "xdg-open %u";
+    case PREF_VCARD_PHOTO_CMD:
+        return "xdg-open %p";
     case PREF_COMPOSE_EDITOR:
     {
         gchar* editor = getenv("EDITOR");
diff --git a/src/config/preferences.h b/src/config/preferences.h
index 2ef6204f..5e856573 100644
--- a/src/config/preferences.h
+++ b/src/config/preferences.h
@@ -105,6 +105,7 @@ typedef enum {
     PREF_TIME_XMLCONSOLE,
     PREF_TIME_STATUSBAR,
     PREF_TIME_LASTACTIVITY,
+    PREF_TIME_VCARD,
     PREF_STATUSES,
     PREF_STATUSES_CONSOLE,
     PREF_STATUSES_CHAT,
@@ -181,6 +182,7 @@ typedef enum {
     PREF_NOTIFY_ROOM_OFFLINE,
     PREF_OX_LOG,
     PREF_MOOD,
+    PREF_VCARD_PHOTO_CMD,
 } preference_t;
 
 typedef struct prof_alias_t
diff --git a/src/event/common.c b/src/event/common.c
index 5c17ca9a..84bc0d58 100644
--- a/src/event/common.c
+++ b/src/event/common.c
@@ -41,6 +41,7 @@
 #include "xmpp/roster_list.h"
 #include "xmpp/muc.h"
 #include "xmpp/xmpp.h"
+#include "xmpp/vcard_funcs.h"
 #include "database.h"
 #include "tools/bookmark_ignore.h"
 
@@ -73,6 +74,7 @@ ev_disconnect_cleanup(void)
 #endif
     log_database_close();
     bookmark_ignore_on_disconnect();
+    vcard_user_free();
 }
 
 gboolean
diff --git a/src/event/server_events.c b/src/event/server_events.c
index 57f73c06..df796448 100644
--- a/src/event/server_events.c
+++ b/src/event/server_events.c
@@ -61,6 +61,7 @@
 #include "xmpp/chat_session.h"
 #include "xmpp/roster_list.h"
 #include "xmpp/avatar.h"
+#include "xmpp/vcard_funcs.h"
 
 #ifdef HAVE_LIBOTR
 #include "otr/otr.h"
@@ -101,7 +102,7 @@ sv_ev_login_account_success(char* account_name, gboolean secured)
 #endif
 
     log_database_init(account);
-
+    vcard_user_refresh();
     avatar_pep_subscribe();
 
     ui_handle_login_account_success(account, secured);
diff --git a/src/ui/console.c b/src/ui/console.c
index 822e6d3e..e7b656f5 100644
--- a/src/ui/console.c
+++ b/src/ui/console.c
@@ -1517,6 +1517,10 @@ cons_time_setting(void)
     char* pref_time_lastactivity = prefs_get_string(PREF_TIME_LASTACTIVITY);
     cons_show("Time last activity (/time)          : %s", pref_time_lastactivity);
     g_free(pref_time_lastactivity);
+
+    char* pref_time_vcard = prefs_get_string(PREF_TIME_VCARD);
+    cons_show("Time vCard (/time)                  : %s", pref_time_vcard);
+    g_free(pref_time_vcard);
 }
 
 void
@@ -2262,6 +2266,10 @@ cons_executable_setting(void)
     gchar* editor = prefs_get_string(PREF_COMPOSE_EDITOR);
     cons_show("Default '/editor' command (/executable editor)                           : %s", editor);
     g_free(editor);
+
+    gchar* vcard_cmd = prefs_get_string(PREF_VCARD_PHOTO_CMD);
+    cons_show("Default '/vcard photo open' command (/executable vcard_photo)            : %s", vcard_cmd);
+    g_free(vcard_cmd);
 }
 
 void
diff --git a/src/ui/ui.h b/src/ui/ui.h
index a3377690..78632aa0 100644
--- a/src/ui/ui.h
+++ b/src/ui/ui.h
@@ -230,6 +230,11 @@ char* confwin_get_string(ProfConfWin* confwin);
 void xmlwin_show(ProfXMLWin* xmlwin, const char* const msg);
 char* xmlwin_get_string(ProfXMLWin* xmlwin);
 
+// vCard window
+void vcardwin_show_vcard_config(ProfVcardWin* vcardwin);
+char* vcardwin_get_string(ProfVcardWin* vcardwin);
+void vcardwin_update(void);
+
 // Input window
 char* inp_readline(void);
 void inp_nonblocking(gboolean reset);
@@ -371,6 +376,7 @@ ProfWin* win_create_muc(const char* const roomjid);
 ProfWin* win_create_config(const char* const title, DataForm* form, ProfConfWinCallback submit, ProfConfWinCallback cancel, const void* userdata);
 ProfWin* win_create_private(const char* const fulljid);
 ProfWin* win_create_plugin(const char* const plugin_name, const char* const tag);
+ProfWin* win_create_vcard(vCard* vcard);
 void win_update_virtual(ProfWin* window);
 void win_free(ProfWin* window);
 gboolean win_notify_remind(ProfWin* window);
@@ -396,6 +402,8 @@ void win_show_occupant(ProfWin* window, Occupant* occupant);
 void win_show_occupant_info(ProfWin* window, const char* const room, Occupant* occupant);
 void win_show_contact(ProfWin* window, PContact contact);
 void win_show_info(ProfWin* window, PContact contact);
+void win_show_vcard(ProfWin* window, vCard* vcard);
+
 void win_clear(ProfWin* window);
 char* win_get_tab_identifier(ProfWin* window);
 char* win_to_string(ProfWin* window);
diff --git a/src/ui/vcardwin.c b/src/ui/vcardwin.c
new file mode 100644
index 00000000..a49f240c
--- /dev/null
+++ b/src/ui/vcardwin.c
@@ -0,0 +1,75 @@
+/*
+ * vcardwin.c
+ * vim: expandtab:ts=4:sts=4:sw=4
+ *
+ * Copyright (C) 2022 Marouane L. <techmetx11@disroot.org>
+ *
+ * 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.
+ *
+ */
+
+#include "ui/ui.h"
+#include "ui/window_list.h"
+#include "xmpp/vcard.h"
+
+void
+vcardwin_show_vcard_config(ProfVcardWin* vcardwin)
+{
+    ProfWin* window = &vcardwin->window;
+
+    win_clear(window);
+    win_show_vcard(window, vcardwin->vcard);
+
+    win_println(window, THEME_DEFAULT, "-", "Use '/vcard save' to save changes.");
+    win_println(window, THEME_DEFAULT, "-", "Use '/help vcard' for more information.");
+}
+
+char*
+vcardwin_get_string(ProfVcardWin* vcardwin)
+{
+    GString* string = g_string_new("vCard: ");
+    char* jid = connection_get_barejid();
+    g_string_append(string, jid);
+
+    if (vcardwin->vcard && vcardwin->vcard->modified) {
+        g_string_append(string, " (modified)");
+    }
+
+    free(jid);
+    return g_string_free(string, FALSE);
+}
+
+void
+vcardwin_update(void)
+{
+    ProfVcardWin* win = wins_get_vcard();
+
+    if (win) {
+        vcardwin_show_vcard_config(win);
+    }
+}
diff --git a/src/ui/win_types.h b/src/ui/win_types.h
index 70d226b0..b7cd3519 100644
--- a/src/ui/win_types.h
+++ b/src/ui/win_types.h
@@ -52,6 +52,7 @@
 #include "tools/autocomplete.h"
 #include "ui/buffer.h"
 #include "xmpp/chat_state.h"
+#include "xmpp/vcard.h"
 
 #define LAYOUT_SPLIT_MEMCHECK   12345671
 #define PROFCHATWIN_MEMCHECK    22374522
@@ -60,6 +61,7 @@
 #define PROFCONFWIN_MEMCHECK    64334685
 #define PROFXMLWIN_MEMCHECK     87333463
 #define PROFPLUGINWIN_MEMCHECK  43434777
+#define PROFVCARDWIN_MEMCHECK   68947523
 
 typedef enum {
     FIELD_HIDDEN,
@@ -140,7 +142,8 @@ typedef enum {
     WIN_CONFIG,
     WIN_PRIVATE,
     WIN_XML,
-    WIN_PLUGIN
+    WIN_PLUGIN,
+    WIN_VCARD
 } win_type_t;
 
 typedef struct prof_win_t
@@ -239,4 +242,11 @@ typedef struct prof_plugin_win_t
     unsigned long memcheck;
 } ProfPluginWin;
 
+typedef struct prof_vcard_win_t
+{
+    ProfWin window;
+    vCard* vcard;
+    unsigned long memcheck;
+} ProfVcardWin;
+
 #endif
diff --git a/src/ui/window.c b/src/ui/window.c
index e25002d6..fbff3374 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -281,6 +281,19 @@ win_create_plugin(const char* const plugin_name, const char* const tag)
     return &new_win->window;
 }
 
+ProfWin*
+win_create_vcard(vCard* vcard)
+{
+    ProfVcardWin* new_win = malloc(sizeof(ProfVcardWin));
+    new_win->window.type = WIN_VCARD;
+    new_win->window.layout = _win_create_simple_layout();
+
+    new_win->vcard = vcard;
+    new_win->memcheck = PROFVCARDWIN_MEMCHECK;
+
+    return &new_win->window;
+}
+
 char*
 win_get_title(ProfWin* window)
 {
@@ -351,7 +364,22 @@ win_get_title(ProfWin* window)
         assert(pluginwin->memcheck == PROFPLUGINWIN_MEMCHECK);
         return strdup(pluginwin->tag);
     }
+    if (window->type == WIN_VCARD) {
+        ProfVcardWin* vcardwin = (ProfVcardWin*)window;
+        assert(vcardwin->memcheck == PROFVCARDWIN_MEMCHECK);
+
+        GString* title = g_string_new("vCard ");
+        char* jid = connection_get_barejid();
+
+        g_string_append(title, jid);
+
+        if (vcardwin->vcard->modified) {
+            g_string_append(title, " *");
+        }
 
+        free(jid);
+        return g_string_free(title, FALSE);
+    }
     return NULL;
 }
 
@@ -474,6 +502,11 @@ win_to_string(ProfWin* window)
         g_string_free(gstring, FALSE);
         return res;
     }
+    case WIN_VCARD:
+    {
+        ProfVcardWin* vcardwin = (ProfVcardWin*)window;
+        return vcardwin_get_string(vcardwin);
+    }
     default:
         return NULL;
     }
@@ -1083,6 +1116,162 @@ win_show_info(ProfWin* window, PContact contact)
 }
 
 void
+win_show_vcard(ProfWin* window, vCard* vcard)
+{
+    GList* pointer;
+    int index = 0;
+
+    if (vcard->fullname) {
+        win_println(window, THEME_DEFAULT, "!", "Full name: %s", vcard->fullname);
+    }
+    if (vcard->name.family || vcard->name.given || vcard->name.middle || vcard->name.prefix || vcard->name.suffix) {
+        win_println(window, THEME_DEFAULT, "!", "Name: ");
+        if (vcard->name.family) {
+            win_println(window, THEME_DEFAULT, "!", "    Family: %s", vcard->name.family);
+        }
+        if (vcard->name.given) {
+            win_println(window, THEME_DEFAULT, "!", "    Given: %s", vcard->name.given);
+        }
+        if (vcard->name.middle) {
+            win_println(window, THEME_DEFAULT, "!", "    Middle: %s", vcard->name.middle);
+        }
+        if (vcard->name.prefix) {
+            win_println(window, THEME_DEFAULT, "!", "    Prefix: %s", vcard->name.prefix);
+        }
+        if (vcard->name.suffix) {
+            win_println(window, THEME_DEFAULT, "!", "    Suffix: %s", vcard->name.suffix);
+        }
+    }
+    index = 0;
+    for (pointer = g_queue_peek_head_link(vcard->elements); pointer != NULL; pointer = pointer->next, index++) {
+        assert(pointer->data != NULL);
+        vcard_element_t* element = (vcard_element_t*)pointer->data;
+
+        switch (element->type) {
+        case VCARD_NICKNAME:
+        {
+            win_println(window, THEME_DEFAULT, "!", "[%d] Nickname: %s", index, element->nickname);
+            break;
+        }
+        case VCARD_PHOTO:
+        {
+            if (element->photo.external) {
+                win_println(window, THEME_DEFAULT, "!", "[%d] Photo, External value: %s", index, element->photo.extval);
+            } else {
+                win_println(window, THEME_DEFAULT, "!", "[%d] Photo (%s), size: %zu", index, element->photo.type, element->photo.length);
+            }
+            break;
+        }
+        case VCARD_BIRTHDAY:
+        {
+            char* date_format = prefs_get_string(PREF_TIME_VCARD);
+            gchar* date = g_date_time_format(element->birthday, date_format);
+            g_free(date_format);
+
+            assert(date != NULL);
+            win_println(window, THEME_DEFAULT, "!", "[%d] Birthday: %s", index, date);
+            g_free(date);
+            break;
+        }
+        case VCARD_ADDRESS:
+        {
+            // Print the header with flags
+            win_println(window, THEME_DEFAULT, "!", "[%d] Address%s%s%s%s%s%s%s", index,
+                        (element->address.options & VCARD_HOME) ? " [home]" : "",
+                        (element->address.options & VCARD_WORK) == VCARD_WORK ? " [work]" : "",
+                        (element->address.options & VCARD_POSTAL) == VCARD_POSTAL ? " [postal]" : "",
+                        (element->address.options & VCARD_PARCEL) == VCARD_PARCEL ? " [parcel]" : "",
+                        (element->address.options & VCARD_INTL) == VCARD_INTL ? " [international]" : "",
+                        (element->address.options & VCARD_DOM) == VCARD_DOM ? " [domestic]" : "",
+                        (element->address.options & VCARD_PREF) == VCARD_PREF ? " [preferred]" : "");
+
+            if (element->address.pobox) {
+                win_println(window, THEME_DEFAULT, "!", "    P.O. Box: %s", element->address.pobox);
+            }
+            if (element->address.extaddr) {
+                win_println(window, THEME_DEFAULT, "!", "    Extended address: %s", element->address.extaddr);
+            }
+            if (element->address.street) {
+                win_println(window, THEME_DEFAULT, "!", "    Street: %s", element->address.street);
+            }
+            if (element->address.locality) {
+                win_println(window, THEME_DEFAULT, "!", "    Locality: %s", element->address.locality);
+            }
+            if (element->address.region) {
+                win_println(window, THEME_DEFAULT, "!", "    Region: %s", element->address.region);
+            }
+            if (element->address.pcode) {
+                win_println(window, THEME_DEFAULT, "!", "    Postal code: %s", element->address.pcode);
+            }
+            if (element->address.country) {
+                win_println(window, THEME_DEFAULT, "!", "    Country: %s", element->address.country);
+            }
+            break;
+        }
+        case VCARD_TELEPHONE:
+        {
+            // Print the header with flags
+            win_println(window, THEME_DEFAULT, "!", "[%d] Telephone%s%s%s%s%s%s%s%s%s%s%s%s%s", index,
+                        (element->telephone.options & VCARD_HOME) ? " [home]" : "",
+                        (element->telephone.options & VCARD_WORK) == VCARD_WORK ? " [work]" : "",
+                        (element->telephone.options & VCARD_TEL_VOICE) == VCARD_TEL_VOICE ? " [voice]" : "",
+                        (element->telephone.options & VCARD_TEL_FAX) == VCARD_TEL_FAX ? " [fax]" : "",
+                        (element->telephone.options & VCARD_TEL_PAGER) == VCARD_TEL_PAGER ? " [pager]" : "",
+                        (element->telephone.options & VCARD_TEL_MSG) == VCARD_TEL_MSG ? " [msg]" : "",
+                        (element->telephone.options & VCARD_TEL_CELL) == VCARD_TEL_CELL ? " [cell]" : "",
+                        (element->telephone.options & VCARD_TEL_VIDEO) == VCARD_TEL_VIDEO ? " [video]" : "",
+                        (element->telephone.options & VCARD_TEL_BBS) == VCARD_TEL_BBS ? " [bbs]" : "",
+                        (element->telephone.options & VCARD_TEL_MODEM) == VCARD_TEL_MODEM ? " [modem]" : "",
+                        (element->telephone.options & VCARD_TEL_ISDN) == VCARD_TEL_ISDN ? " [isdn]" : "",
+                        (element->telephone.options & VCARD_TEL_PCS) == VCARD_TEL_PCS ? " [pcs]" : "",
+                        (element->telephone.options & VCARD_PREF) == VCARD_PREF ? " [preferred]" : "");
+            if (element->telephone.number) {
+                win_println(window, THEME_DEFAULT, "!", "    Number: %s", element->telephone.number);
+            }
+            break;
+        }
+        case VCARD_EMAIL:
+        {
+            // Print the header with flags
+            win_println(window, THEME_DEFAULT, "!", "[%d] E-mail%s%s%s%s%s", index,
+                        (element->email.options & VCARD_HOME) ? " [home]" : "",
+                        (element->email.options & VCARD_WORK) == VCARD_WORK ? " [work]" : "",
+                        (element->email.options & VCARD_EMAIL_X400) == VCARD_EMAIL_X400 ? " [x400]" : "",
+                        (element->email.options & VCARD_EMAIL_INTERNET) == VCARD_EMAIL_INTERNET ? " [internet]" : "",
+                        (element->email.options & VCARD_PREF) == VCARD_PREF ? " [preferred]" : "");
+            if (element->email.userid) {
+                win_println(window, THEME_DEFAULT, "!", "    ID: %s", element->email.userid);
+            }
+            break;
+        }
+        case VCARD_JID:
+        {
+            win_println(window, THEME_DEFAULT, "!", "[%d] Jabber ID: %s", index, element->jid);
+            break;
+        }
+        case VCARD_TITLE:
+        {
+            win_println(window, THEME_DEFAULT, "!", "[%d] Title: %s", index, element->title);
+            break;
+        }
+        case VCARD_ROLE:
+        {
+            win_println(window, THEME_DEFAULT, "!", "[%d] Role: %s", index, element->role);
+            break;
+        }
+        case VCARD_NOTE:
+        {
+            win_println(window, THEME_DEFAULT, "!", "[%d] Note: %s", index, element->note);
+            break;
+        }
+        case VCARD_URL:
+            win_println(window, THEME_DEFAULT, "!", "[%d] URL: %s", index, element->url);
+            break;
+        }
+    }
+}
+
+void
 win_show_status_string(ProfWin* window, const char* const from,
                        const char* const show, const char* const status,
                        GDateTime* last_activity, const char* const pre,
diff --git a/src/ui/window_list.c b/src/ui/window_list.c
index 56370bca..cdb87de4 100644
--- a/src/ui/window_list.c
+++ b/src/ui/window_list.c
@@ -711,6 +711,18 @@ wins_new_plugin(const char* const plugin_name, const char* const tag)
     return newwin;
 }
 
+ProfWin*
+wins_new_vcard(vCard* vcard)
+{
+    GList* keys = g_hash_table_get_keys(windows);
+    int result = _wins_get_next_available_num(keys);
+    g_list_free(keys);
+    ProfWin* newwin = win_create_vcard(vcard);
+    g_hash_table_insert(windows, GINT_TO_POINTER(result), newwin);
+
+    return newwin;
+}
+
 gboolean
 wins_do_notify_remind(void)
 {
@@ -805,6 +817,27 @@ wins_get_xmlconsole(void)
     return NULL;
 }
 
+ProfVcardWin*
+wins_get_vcard(void)
+{
+    GList* values = g_hash_table_get_values(windows);
+    GList* curr = values;
+
+    while (curr) {
+        ProfWin* window = curr->data;
+        if (window->type == WIN_VCARD) {
+            ProfVcardWin* vcardwin = (ProfVcardWin*)window;
+            assert(vcardwin->memcheck == PROFVCARDWIN_MEMCHECK);
+            g_list_free(values);
+            return vcardwin;
+        }
+        curr = g_list_next(curr);
+    }
+
+    g_list_free(values);
+    return NULL;
+}
+
 GSList*
 wins_get_chat_recipients(void)
 {
diff --git a/src/ui/window_list.h b/src/ui/window_list.h
index bcb18d14..788248b9 100644
--- a/src/ui/window_list.h
+++ b/src/ui/window_list.h
@@ -46,6 +46,7 @@ ProfWin* wins_new_muc(const char* const roomjid);
 ProfWin* wins_new_config(const char* const roomjid, DataForm* form, ProfConfWinCallback submit, ProfConfWinCallback cancel, const void* userdata);
 ProfWin* wins_new_private(const char* const fulljid);
 ProfWin* wins_new_plugin(const char* const plugin_name, const char* const tag);
+ProfWin* wins_new_vcard(vCard* vcard);
 
 gboolean wins_chat_exists(const char* const barejid);
 GList* wins_get_private_chats(const char* const roomjid);
@@ -61,6 +62,7 @@ ProfConfWin* wins_get_conf(const char* const roomjid);
 ProfPrivateWin* wins_get_private(const char* const fulljid);
 ProfPluginWin* wins_get_plugin(const char* const tag);
 ProfXMLWin* wins_get_xmlconsole(void);
+ProfVcardWin* wins_get_vcard(void);
 
 void wins_close_plugin(char* tag);
 
diff --git a/src/xmpp/stanza.c b/src/xmpp/stanza.c
index 6b8377b6..fc2fcdc6 100644
--- a/src/xmpp/stanza.c
+++ b/src/xmpp/stanza.c
@@ -2688,6 +2688,25 @@ stanza_create_avatar_metadata_publish_iq(xmpp_ctx_t* ctx, const char* img_data,
 }
 
 xmpp_stanza_t*
+stanza_create_vcard_request_iq(xmpp_ctx_t* ctx, const char* const jid, const char* const stanza_id)
+{
+    xmpp_stanza_t* iq = xmpp_iq_new(ctx, STANZA_TYPE_GET, stanza_id);
+    xmpp_stanza_set_from(iq, connection_get_fulljid());
+    if (jid) {
+        xmpp_stanza_set_to(iq, jid);
+    }
+
+    xmpp_stanza_t* vcard = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(vcard, STANZA_NAME_VCARD);
+    xmpp_stanza_set_ns(vcard, STANZA_NS_VCARD);
+
+    xmpp_stanza_add_child(iq, vcard);
+    xmpp_stanza_release(vcard);
+
+    return iq;
+}
+
+xmpp_stanza_t*
 stanza_attach_correction(xmpp_ctx_t* ctx, xmpp_stanza_t* stanza, const char* const replace_id)
 {
     xmpp_stanza_t* replace_stanza = xmpp_stanza_new(ctx);
diff --git a/src/xmpp/stanza.h b/src/xmpp/stanza.h
index 12c9a5ee..ddd46e9a 100644
--- a/src/xmpp/stanza.h
+++ b/src/xmpp/stanza.h
@@ -123,6 +123,7 @@
 #define STANZA_NAME_MOOD             "mood"
 #define STANZA_NAME_RECEIVED         "received"
 #define STANZA_NAME_SENT             "sent"
+#define STANZA_NAME_VCARD            "vCard"
 
 // error conditions
 #define STANZA_NAME_BAD_REQUEST             "bad-request"
@@ -249,6 +250,7 @@
 #define STANZA_NS_MOOD_NOTIFY             "http://jabber.org/protocol/mood+notify"
 #define STANZA_NS_STREAMS                 "http://etherx.jabber.org/streams"
 #define STANZA_NS_XMPP_STREAMS            "urn:ietf:params:xml:ns:xmpp-streams"
+#define STANZA_NS_VCARD                   "vcard-temp"
 
 #define STANZA_DATAFORM_SOFTWARE "urn:xmpp:dataforms:softwareinfo"
 
@@ -413,6 +415,7 @@ void stanza_free_caps(XMPPCaps* caps);
 xmpp_stanza_t* stanza_create_avatar_retrieve_data_request(xmpp_ctx_t* ctx, const char* stanza_id, const char* const item_id, const char* const jid);
 xmpp_stanza_t* stanza_create_avatar_data_publish_iq(xmpp_ctx_t* ctx, const char* img_data, gsize len);
 xmpp_stanza_t* stanza_create_avatar_metadata_publish_iq(xmpp_ctx_t* ctx, const char* img_data, gsize len, int height, int width);
+xmpp_stanza_t* stanza_create_vcard_request_iq(xmpp_ctx_t* ctx, const char* const jid, const char* const stanza_id);
 xmpp_stanza_t* stanza_create_mam_iq(xmpp_ctx_t* ctx, const char* const jid, const char* const startdate, const char* const lastid);
 xmpp_stanza_t* stanza_change_password(xmpp_ctx_t* ctx, const char* const user, const char* const password);
 xmpp_stanza_t* stanza_register_new_account(xmpp_ctx_t* ctx, const char* const user, const char* const password);
diff --git a/src/xmpp/vcard.c b/src/xmpp/vcard.c
new file mode 100644
index 00000000..6d2f0b1e
--- /dev/null
+++ b/src/xmpp/vcard.c
@@ -0,0 +1,1613 @@
+/*
+ * vcard.c
+ * vim: expandtab:ts=4:sts=4:sw=4
+ *
+ * Copyright (C) 2022 Marouane L. <techmetx11@disroot.org>
+ *
+ * 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.
+ *
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <glib.h>
+#include <strophe.h>
+#include <sys/stat.h>
+
+#include "xmpp/vcard.h"
+#include "config/files.h"
+#include "config/preferences.h"
+#include "ui/ui.h"
+#include "ui/window_list.h"
+#include "xmpp/connection.h"
+#include "xmpp/iq.h"
+#include "xmpp/stanza.h"
+
+// Connected account's vCard
+vCard* vcard_user = NULL;
+
+typedef struct
+{
+    vCard* vcard;
+    ProfWin* window;
+
+    // for photo
+    int photo_index;
+    gboolean open;
+    char* filename;
+} _userdata;
+
+static void
+_free_vcard_element(void* velement)
+{
+    vcard_element_t* element = (vcard_element_t*)velement;
+
+    switch (element->type) {
+    case VCARD_NICKNAME:
+    {
+        if (element->nickname) {
+            free(element->nickname);
+        }
+        break;
+    }
+    case VCARD_PHOTO:
+    {
+        if (element->photo.external) {
+            if (element->photo.extval) {
+                free(element->photo.extval);
+            }
+        } else {
+            if (element->photo.data) {
+                g_free(element->photo.data);
+            }
+            if (element->photo.type) {
+                free(element->photo.type);
+            }
+        }
+        break;
+    }
+    case VCARD_BIRTHDAY:
+    {
+        g_date_time_unref(element->birthday);
+        break;
+    }
+    case VCARD_ADDRESS:
+    {
+        if (element->address.pobox) {
+            free(element->address.pobox);
+        }
+        if (element->address.extaddr) {
+            free(element->address.extaddr);
+        }
+        if (element->address.street) {
+            free(element->address.street);
+        }
+        if (element->address.locality) {
+            free(element->address.locality);
+        }
+        if (element->address.region) {
+            free(element->address.region);
+        }
+        if (element->address.pcode) {
+            free(element->address.pcode);
+        }
+        if (element->address.country) {
+            free(element->address.country);
+        }
+        break;
+    }
+    case VCARD_TELEPHONE:
+    {
+        if (element->telephone.number) {
+            free(element->telephone.number);
+        }
+        break;
+    }
+    case VCARD_EMAIL:
+    {
+        if (element->email.userid) {
+            free(element->email.userid);
+        }
+        break;
+    }
+    case VCARD_JID:
+    {
+        if (element->jid) {
+            free(element->jid);
+        }
+        break;
+    }
+    case VCARD_TITLE:
+    {
+        if (element->title) {
+            free(element->title);
+        }
+        break;
+    }
+    case VCARD_ROLE:
+    {
+        if (element->role) {
+            free(element->role);
+        }
+        break;
+    }
+    case VCARD_NOTE:
+    {
+        if (element->note) {
+            free(element->note);
+        }
+        break;
+    }
+    case VCARD_URL:
+        if (element->url) {
+            free(element->url);
+        }
+        break;
+    }
+
+    free(element);
+}
+
+void
+vcard_free_full(vCard* vcard)
+{
+    if (!vcard) {
+        return;
+    }
+
+    // Free the fullname element
+    if (vcard->fullname) {
+        free(vcard->fullname);
+    }
+
+    // Free the name element
+    if (vcard->name.family) {
+        free(vcard->name.family);
+    }
+    if (vcard->name.given) {
+        free(vcard->name.given);
+    }
+    if (vcard->name.middle) {
+        free(vcard->name.middle);
+    }
+    if (vcard->name.prefix) {
+        free(vcard->name.prefix);
+    }
+    if (vcard->name.suffix) {
+        free(vcard->name.suffix);
+    }
+
+    // Clear the elements queue
+    g_queue_clear_full(vcard->elements, _free_vcard_element);
+}
+
+vCard*
+vcard_new(void)
+{
+    vCard* vcard = calloc(1, sizeof(vCard));
+
+    if (!vcard) {
+        return NULL;
+    }
+
+    vcard->elements = g_queue_new();
+
+    return vcard;
+}
+
+void
+vcard_free(vCard* vcard)
+{
+    if (!vcard) {
+        return;
+    }
+
+    g_queue_free(vcard->elements);
+    free(vcard);
+}
+
+static void
+_free_userdata(_userdata* data)
+{
+    vcard_free_full(data->vcard);
+    vcard_free(data->vcard);
+
+    if (data->filename) {
+        free(data->filename);
+    }
+
+    free(data);
+}
+
+// Function must be called with <vCard> root element
+gboolean
+vcard_parse(xmpp_stanza_t* vcard_xml, vCard* vcard)
+{
+    // 2 bits
+    // 01 = fullname is set
+    // 10 = name is set
+    // Fullname and name can only be set once, if another
+    // fullname, or name element is found, then it will be
+    // ignored
+    char flags = 0;
+
+    if (!vcard_xml) {
+        // vCard XML is null, stop
+        return FALSE;
+    }
+
+    // Pointer to children of the vCard element
+    xmpp_stanza_t* child_pointer = xmpp_stanza_get_children(vcard_xml);
+
+    // Connection context, for freeing elements allocated by Strophe
+    const xmpp_ctx_t* ctx = connection_get_ctx();
+
+    // Pointer to the children of the children
+    xmpp_stanza_t* child_pointer2 = NULL;
+
+    for (; child_pointer != NULL; child_pointer = xmpp_stanza_get_next(child_pointer)) {
+        if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "FN") && !(flags & 1)) {
+            vcard->fullname = stanza_text_strdup(child_pointer);
+            flags |= 1;
+        } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "N") && !(flags & 2)) {
+            child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "FAMILY");
+            vcard->name.family = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL;
+            child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "GIVEN");
+            vcard->name.given = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL;
+            child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "MIDDLE");
+            vcard->name.middle = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL;
+            child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "PREFIX");
+            vcard->name.prefix = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL;
+            child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "SUFFIX");
+            vcard->name.suffix = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL;
+            flags |= 2;
+        } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "NICKNAME")) {
+            vcard_element_t* element = calloc(1, sizeof(vcard_element_t));
+            if (!element) {
+                // Allocation failed
+                continue;
+            }
+
+            element->type = VCARD_NICKNAME;
+            element->nickname = stanza_text_strdup(child_pointer);
+
+            if (!element->nickname) {
+                // Invaild element, free and do not push
+                free(element);
+                continue;
+            }
+
+            g_queue_push_tail(vcard->elements, element);
+        } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "PHOTO")) {
+            vcard_element_t* element = calloc(1, sizeof(vcard_element_t));
+            if (!element) {
+                // Allocation failed
+                continue;
+            }
+
+            element->type = VCARD_PHOTO;
+
+            child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "EXTVAL");
+            if (child_pointer2) {
+                element->photo.external = TRUE;
+                element->photo.extval = stanza_text_strdup(child_pointer2);
+            } else {
+                element->photo.external = FALSE;
+                child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "BINVAL");
+                if (!child_pointer2) {
+                    // No EXTVAL or BINVAL, invalid photo, skipping
+                    free(element);
+                    continue;
+                }
+                char* photo_base64 = xmpp_stanza_get_text(child_pointer2);
+                if (!photo_base64) {
+                    free(element);
+                    continue;
+                }
+                element->photo.data = g_base64_decode(photo_base64, &element->photo.length);
+                xmpp_free(ctx, photo_base64);
+
+                child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "TYPE");
+                if (!child_pointer2) {
+                    // No TYPE, invalid photo, skipping
+                    if (element->photo.data) {
+                        g_free(element->photo.data);
+                    }
+                    free(element);
+                    continue;
+                }
+
+                element->photo.type = stanza_text_strdup(child_pointer2);
+            }
+            g_queue_push_tail(vcard->elements, element);
+        } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "BDAY")) {
+            vcard_element_t* element = calloc(1, sizeof(vcard_element_t));
+            if (!element) {
+                // Allocation failed
+                continue;
+            }
+
+            element->type = VCARD_BIRTHDAY;
+
+            char* bday_text = xmpp_stanza_get_text(child_pointer);
+            if (!bday_text) {
+                free(element);
+                continue;
+            }
+
+            // Check if the birthday string is a date/time or date only string
+            char* check_pointer = bday_text;
+            gboolean is_datetime = FALSE;
+            for (; *check_pointer != 0; check_pointer++) {
+                if (*check_pointer == 'T' || *check_pointer == 't' || *check_pointer == ' ') {
+                    is_datetime = TRUE;
+                    break;
+                }
+            }
+
+            if (!is_datetime) {
+                // glib doesn't parse ISO8601 date only strings, so we're gonna
+                // add a time string to make it parse
+                GString* date_string = g_string_new(bday_text);
+                g_string_append(date_string, "T00:00:00Z");
+
+                element->birthday = g_date_time_new_from_iso8601(date_string->str, NULL);
+
+                g_string_free(date_string, TRUE);
+            } else {
+                element->birthday = g_date_time_new_from_iso8601(bday_text, NULL);
+            }
+
+            xmpp_free(ctx, bday_text);
+
+            if (!element->birthday) {
+                // Invalid birthday ISO 8601 date, skipping
+                free(element);
+                continue;
+            }
+            g_queue_push_tail(vcard->elements, element);
+        } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "ADR")) {
+            vcard_element_t* element = calloc(1, sizeof(vcard_element_t));
+            if (!element) {
+                // Allocation failed
+                continue;
+            }
+
+            element->type = VCARD_ADDRESS;
+            if (xmpp_stanza_get_child_by_name(child_pointer, "HOME")) {
+                element->address.options |= VCARD_HOME;
+            }
+            if (xmpp_stanza_get_child_by_name(child_pointer, "WORK")) {
+                element->address.options |= VCARD_WORK;
+            }
+            if (xmpp_stanza_get_child_by_name(child_pointer, "POSTAL")) {
+                element->address.options |= VCARD_POSTAL;
+            }
+            if (xmpp_stanza_get_child_by_name(child_pointer, "PARCEL")) {
+                element->address.options |= VCARD_PARCEL;
+            }
+
+            if (xmpp_stanza_get_child_by_name(child_pointer, "DOM")) {
+                element->address.options |= VCARD_DOM;
+            } else if (xmpp_stanza_get_child_by_name(child_pointer, "INTL")) {
+                element->address.options |= VCARD_INTL;
+            }
+
+            if (xmpp_stanza_get_child_by_name(child_pointer, "PREF")) {
+                element->address.options |= VCARD_PREF;
+            }
+
+            child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "POBOX");
+            element->address.pobox = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL;
+            child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "EXTADD");
+            element->address.extaddr = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL;
+            child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "STREET");
+            element->address.street = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL;
+            child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "LOCALITY");
+            element->address.locality = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL;
+            child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "REGION");
+            element->address.region = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL;
+            child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "PCODE");
+            element->address.pcode = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL;
+            child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "CTRY");
+            element->address.country = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL;
+            g_queue_push_tail(vcard->elements, element);
+        } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "TEL")) {
+            vcard_element_t* element = calloc(1, sizeof(vcard_element_t));
+            if (!element) {
+                // Allocation failed
+                continue;
+            }
+
+            element->type = VCARD_TELEPHONE;
+            if (xmpp_stanza_get_child_by_name(child_pointer, "HOME")) {
+                element->telephone.options |= VCARD_HOME;
+            }
+            if (xmpp_stanza_get_child_by_name(child_pointer, "WORK")) {
+                element->telephone.options |= VCARD_WORK;
+            }
+            if (xmpp_stanza_get_child_by_name(child_pointer, "VOICE")) {
+                element->telephone.options |= VCARD_TEL_VOICE;
+            }
+            if (xmpp_stanza_get_child_by_name(child_pointer, "FAX")) {
+                element->telephone.options |= VCARD_TEL_FAX;
+            }
+            if (xmpp_stanza_get_child_by_name(child_pointer, "PAGER")) {
+                element->telephone.options |= VCARD_TEL_PAGER;
+            }
+            if (xmpp_stanza_get_child_by_name(child_pointer, "MSG")) {
+                element->telephone.options |= VCARD_TEL_MSG;
+            }
+            if (xmpp_stanza_get_child_by_name(child_pointer, "CELL")) {
+                element->telephone.options |= VCARD_TEL_CELL;
+            }
+            if (xmpp_stanza_get_child_by_name(child_pointer, "VIDEO")) {
+                element->telephone.options |= VCARD_TEL_VIDEO;
+            }
+            if (xmpp_stanza_get_child_by_name(child_pointer, "BBS")) {
+                element->telephone.options |= VCARD_TEL_BBS;
+            }
+            if (xmpp_stanza_get_child_by_name(child_pointer, "MODEM")) {
+                element->telephone.options |= VCARD_TEL_MODEM;
+            }
+            if (xmpp_stanza_get_child_by_name(child_pointer, "ISDN")) {
+                element->telephone.options |= VCARD_TEL_ISDN;
+            }
+            if (xmpp_stanza_get_child_by_name(child_pointer, "PCS")) {
+                element->telephone.options |= VCARD_TEL_PCS;
+            }
+            if (xmpp_stanza_get_child_by_name(child_pointer, "PREF")) {
+                element->telephone.options |= VCARD_PREF;
+            }
+
+            child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "NUMBER");
+            if (!child_pointer2) {
+                // No NUMBER, invalid telephone, skipping
+                free(element);
+                continue;
+            }
+            element->telephone.number = stanza_text_strdup(child_pointer2);
+            g_queue_push_tail(vcard->elements, element);
+        } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "EMAIL")) {
+            vcard_element_t* element = calloc(1, sizeof(vcard_element_t));
+            if (!element) {
+                // Allocation failed
+                continue;
+            }
+
+            element->type = VCARD_EMAIL;
+            if (xmpp_stanza_get_child_by_name(child_pointer, "HOME")) {
+                element->email.options |= VCARD_HOME;
+            }
+            if (xmpp_stanza_get_child_by_name(child_pointer, "WORK")) {
+                element->email.options |= VCARD_WORK;
+            }
+            if (xmpp_stanza_get_child_by_name(child_pointer, "INTERNET")) {
+                element->email.options |= VCARD_EMAIL_INTERNET;
+            }
+            if (xmpp_stanza_get_child_by_name(child_pointer, "X400")) {
+                element->email.options |= VCARD_EMAIL_X400;
+            }
+            if (xmpp_stanza_get_child_by_name(child_pointer, "PREF")) {
+                element->email.options |= VCARD_PREF;
+            }
+
+            child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "USERID");
+            if (!child_pointer2) {
+                // No USERID, invalid email, skipping
+                free(element);
+                continue;
+            }
+            element->email.userid = stanza_text_strdup(child_pointer2);
+            g_queue_push_tail(vcard->elements, element);
+        } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "JABBERID")) {
+            vcard_element_t* element = calloc(1, sizeof(vcard_element_t));
+            if (!element) {
+                // Allocation failed
+                continue;
+            }
+
+            element->type = VCARD_JID;
+            element->jid = stanza_text_strdup(child_pointer);
+
+            if (!element->jid) {
+                // Invalid element, free and do not push
+                free(element);
+                continue;
+            }
+
+            g_queue_push_tail(vcard->elements, element);
+        } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "TITLE")) {
+            vcard_element_t* element = calloc(1, sizeof(vcard_element_t));
+            if (!element) {
+                // Allocation failed
+                continue;
+            }
+
+            element->type = VCARD_TITLE;
+            element->title = stanza_text_strdup(child_pointer);
+
+            if (!element->title) {
+                // Invalid element, free and do not push
+                free(element);
+                continue;
+            }
+
+            g_queue_push_tail(vcard->elements, element);
+        } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "ROLE")) {
+            vcard_element_t* element = calloc(1, sizeof(vcard_element_t));
+            if (!element) {
+                // Allocation failed
+                continue;
+            }
+
+            element->type = VCARD_ROLE;
+            element->role = stanza_text_strdup(child_pointer);
+
+            if (!element->role) {
+                // Invalid element, free and do not push
+                free(element);
+                continue;
+            }
+
+            g_queue_push_tail(vcard->elements, element);
+        } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "NOTE")) {
+            vcard_element_t* element = calloc(1, sizeof(vcard_element_t));
+            if (!element) {
+                // Allocation failed
+                continue;
+            }
+
+            element->type = VCARD_NOTE;
+            element->note = stanza_text_strdup(child_pointer);
+
+            if (!element->note) {
+                // Invalid element, free and do not push
+                free(element);
+                continue;
+            }
+
+            g_queue_push_tail(vcard->elements, element);
+        } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "URL")) {
+            vcard_element_t* element = calloc(1, sizeof(vcard_element_t));
+            if (!element) {
+                // Allocation failed
+                continue;
+            }
+
+            element->type = VCARD_URL;
+            element->url = stanza_text_strdup(child_pointer);
+
+            if (!element->note) {
+                // Invalid element, free and do not push
+                free(element);
+                continue;
+            }
+
+            g_queue_push_tail(vcard->elements, element);
+        }
+    }
+    return TRUE;
+}
+
+xmpp_stanza_t*
+vcard_to_xml(xmpp_ctx_t* const ctx, vCard* vcard)
+{
+    if (!vcard || !ctx) {
+        return NULL;
+    }
+
+    xmpp_stanza_t* vcard_stanza = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(vcard_stanza, STANZA_NAME_VCARD);
+    xmpp_stanza_set_ns(vcard_stanza, STANZA_NS_VCARD);
+
+    if (vcard->fullname) {
+        // <FN> element
+        xmpp_stanza_t* fn = xmpp_stanza_new(ctx);
+        xmpp_stanza_set_name(fn, "FN");
+
+        xmpp_stanza_t* fn_text = xmpp_stanza_new(ctx);
+        xmpp_stanza_set_text(fn_text, vcard->fullname);
+        xmpp_stanza_add_child(fn, fn_text);
+        xmpp_stanza_release(fn_text);
+
+        xmpp_stanza_add_child(vcard_stanza, fn);
+        xmpp_stanza_release(fn);
+    }
+
+    if (vcard->name.family || vcard->name.given || vcard->name.middle || vcard->name.prefix || vcard->name.suffix) {
+        // <NAME> element
+        xmpp_stanza_t* name = xmpp_stanza_new(ctx);
+        xmpp_stanza_set_name(name, "NAME");
+
+        if (vcard->name.family) {
+            xmpp_stanza_t* name_family = xmpp_stanza_new(ctx);
+            xmpp_stanza_set_name(name_family, "FAMILY");
+
+            xmpp_stanza_t* name_family_text = xmpp_stanza_new(ctx);
+            xmpp_stanza_set_text(name_family_text, vcard->name.family);
+            xmpp_stanza_add_child(name_family, name_family_text);
+            xmpp_stanza_release(name_family_text);
+
+            xmpp_stanza_add_child(name, name_family);
+            xmpp_stanza_release(name_family);
+        }
+
+        if (vcard->name.given) {
+            xmpp_stanza_t* name_given = xmpp_stanza_new(ctx);
+            xmpp_stanza_set_name(name_given, "GIVEN");
+
+            xmpp_stanza_t* name_given_text = xmpp_stanza_new(ctx);
+            xmpp_stanza_set_text(name_given_text, vcard->name.given);
+            xmpp_stanza_add_child(name_given, name_given_text);
+            xmpp_stanza_release(name_given_text);
+
+            xmpp_stanza_add_child(name, name_given);
+            xmpp_stanza_release(name_given);
+        }
+
+        if (vcard->name.middle) {
+            xmpp_stanza_t* name_middle = xmpp_stanza_new(ctx);
+            xmpp_stanza_set_name(name_middle, "MIDDLE");
+
+            xmpp_stanza_t* name_middle_text = xmpp_stanza_new(ctx);
+            xmpp_stanza_set_text(name_middle_text, vcard->name.middle);
+            xmpp_stanza_add_child(name_middle, name_middle_text);
+            xmpp_stanza_release(name_middle_text);
+
+            xmpp_stanza_add_child(name, name_middle);
+            xmpp_stanza_release(name_middle);
+        }
+
+        if (vcard->name.prefix) {
+            xmpp_stanza_t* name_prefix = xmpp_stanza_new(ctx);
+            xmpp_stanza_set_name(name_prefix, "PREFIX");
+
+            xmpp_stanza_t* name_prefix_text = xmpp_stanza_new(ctx);
+            xmpp_stanza_set_text(name_prefix_text, vcard->name.prefix);
+            xmpp_stanza_add_child(name_prefix, name_prefix_text);
+            xmpp_stanza_release(name_prefix_text);
+
+            xmpp_stanza_add_child(name, name_prefix);
+            xmpp_stanza_release(name_prefix);
+        }
+
+        if (vcard->name.suffix) {
+            xmpp_stanza_t* name_suffix = xmpp_stanza_new(ctx);
+            xmpp_stanza_set_name(name_suffix, "SUFFIX");
+
+            xmpp_stanza_t* name_suffix_text = xmpp_stanza_new(ctx);
+            xmpp_stanza_set_text(name_suffix_text, vcard->name.suffix);
+            xmpp_stanza_add_child(name_suffix, name_suffix_text);
+            xmpp_stanza_release(name_suffix_text);
+
+            xmpp_stanza_add_child(name, name_suffix);
+            xmpp_stanza_release(name_suffix);
+        }
+
+        xmpp_stanza_add_child(vcard_stanza, name);
+        xmpp_stanza_release(name);
+    }
+
+    GList* pointer = g_queue_peek_head_link(vcard->elements);
+
+    for (; pointer != NULL; pointer = pointer->next) {
+        assert(pointer->data != NULL);
+        vcard_element_t* element = (vcard_element_t*)pointer->data;
+
+        switch (element->type) {
+        case VCARD_NICKNAME:
+        {
+            // <NICKNAME> element
+            xmpp_stanza_t* nickname = xmpp_stanza_new(ctx);
+            xmpp_stanza_set_name(nickname, "NICKNAME");
+
+            xmpp_stanza_t* nickname_text = xmpp_stanza_new(ctx);
+            xmpp_stanza_set_text(nickname_text, element->nickname);
+            xmpp_stanza_add_child(nickname, nickname_text);
+            xmpp_stanza_release(nickname_text);
+
+            xmpp_stanza_add_child(vcard_stanza, nickname);
+            xmpp_stanza_release(nickname);
+            break;
+        }
+        case VCARD_PHOTO:
+        {
+            // <PHOTO> element
+            xmpp_stanza_t* photo = xmpp_stanza_new(ctx);
+            xmpp_stanza_set_name(photo, "PHOTO");
+
+            if (element->photo.external) {
+                xmpp_stanza_t* extval = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(extval, "EXTVAL");
+
+                xmpp_stanza_t* extval_text = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_text(extval_text, element->photo.extval);
+                xmpp_stanza_add_child(extval, extval_text);
+                xmpp_stanza_release(extval_text);
+
+                xmpp_stanza_add_child(photo, extval);
+                xmpp_stanza_release(extval);
+            } else {
+                xmpp_stanza_t* binval = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(binval, "BINVAL");
+
+                gchar* base64 = g_base64_encode(element->photo.data, element->photo.length);
+                xmpp_stanza_t* binval_text = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_text(binval_text, base64);
+                g_free(base64);
+                xmpp_stanza_add_child(binval, binval_text);
+                xmpp_stanza_release(binval_text);
+
+                xmpp_stanza_add_child(photo, binval);
+                xmpp_stanza_release(binval);
+
+                xmpp_stanza_t* type = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(type, "TYPE");
+
+                xmpp_stanza_t* type_text = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_text(type_text, element->photo.type);
+                xmpp_stanza_add_child(type, type_text);
+                xmpp_stanza_release(type_text);
+
+                xmpp_stanza_add_child(photo, type);
+                xmpp_stanza_release(type);
+            }
+
+            xmpp_stanza_add_child(vcard_stanza, photo);
+            xmpp_stanza_release(photo);
+            break;
+        }
+        case VCARD_BIRTHDAY:
+        {
+            // <BDAY> element
+            xmpp_stanza_t* birthday = xmpp_stanza_new(ctx);
+            xmpp_stanza_set_name(birthday, "BDAY");
+
+            gchar* bday_text = g_date_time_format(element->birthday, "%Y-%m-%d");
+            xmpp_stanza_t* birthday_text = xmpp_stanza_new(ctx);
+            xmpp_stanza_set_text(birthday_text, bday_text);
+            g_free(bday_text);
+            xmpp_stanza_add_child(birthday, birthday_text);
+            xmpp_stanza_release(birthday_text);
+
+            xmpp_stanza_add_child(vcard_stanza, birthday);
+            xmpp_stanza_release(birthday);
+            break;
+        }
+        case VCARD_ADDRESS:
+        {
+            // <ADR> element
+            xmpp_stanza_t* address = xmpp_stanza_new(ctx);
+            xmpp_stanza_set_name(address, "ADR");
+
+            // Options
+            if (element->address.options & VCARD_HOME) {
+                xmpp_stanza_t* home = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(home, "HOME");
+
+                xmpp_stanza_add_child(address, home);
+                xmpp_stanza_release(home);
+            }
+            if ((element->address.options & VCARD_WORK) == VCARD_WORK) {
+                xmpp_stanza_t* work = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(work, "WORK");
+
+                xmpp_stanza_add_child(address, work);
+                xmpp_stanza_release(work);
+            }
+            if ((element->address.options & VCARD_POSTAL) == VCARD_POSTAL) {
+                xmpp_stanza_t* postal = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(postal, "POSTAL");
+
+                xmpp_stanza_add_child(address, postal);
+                xmpp_stanza_release(postal);
+            }
+            if ((element->address.options & VCARD_PARCEL) == VCARD_PARCEL) {
+                xmpp_stanza_t* parcel = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(parcel, "PARCEL");
+
+                xmpp_stanza_add_child(address, parcel);
+                xmpp_stanza_release(parcel);
+            }
+            if ((element->address.options & VCARD_INTL) == VCARD_INTL) {
+                xmpp_stanza_t* intl = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(intl, "INTL");
+
+                xmpp_stanza_add_child(address, intl);
+                xmpp_stanza_release(intl);
+            }
+            if ((element->address.options & VCARD_DOM) == VCARD_DOM) {
+                xmpp_stanza_t* dom = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(dom, "DOM");
+
+                xmpp_stanza_add_child(address, dom);
+                xmpp_stanza_release(dom);
+            }
+            if ((element->address.options & VCARD_PREF) == VCARD_PREF) {
+                xmpp_stanza_t* pref = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(pref, "PREF");
+
+                xmpp_stanza_add_child(address, pref);
+                xmpp_stanza_release(pref);
+            }
+
+            if (element->address.pobox) {
+                xmpp_stanza_t* pobox = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(pobox, "POBOX");
+
+                xmpp_stanza_t* pobox_text = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_text(pobox_text, element->address.pobox);
+                xmpp_stanza_add_child(pobox, pobox_text);
+                xmpp_stanza_release(pobox_text);
+
+                xmpp_stanza_add_child(address, pobox);
+                xmpp_stanza_release(pobox);
+            }
+            if (element->address.extaddr) {
+                xmpp_stanza_t* extaddr = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(extaddr, "EXTADD");
+
+                xmpp_stanza_t* extaddr_text = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_text(extaddr_text, element->address.extaddr);
+                xmpp_stanza_add_child(extaddr, extaddr_text);
+                xmpp_stanza_release(extaddr_text);
+
+                xmpp_stanza_add_child(address, extaddr);
+                xmpp_stanza_release(extaddr);
+            }
+            if (element->address.street) {
+                xmpp_stanza_t* street = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(street, "STREET");
+
+                xmpp_stanza_t* street_text = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_text(street_text, element->address.street);
+                xmpp_stanza_add_child(street, street_text);
+                xmpp_stanza_release(street_text);
+
+                xmpp_stanza_add_child(address, street);
+                xmpp_stanza_release(street);
+            }
+            if (element->address.locality) {
+                xmpp_stanza_t* locality = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(locality, "LOCALITY");
+
+                xmpp_stanza_t* locality_text = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_text(locality_text, element->address.locality);
+                xmpp_stanza_add_child(locality, locality_text);
+                xmpp_stanza_release(locality_text);
+
+                xmpp_stanza_add_child(address, locality);
+                xmpp_stanza_release(locality);
+            }
+            if (element->address.region) {
+                xmpp_stanza_t* region = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(region, "REGION");
+
+                xmpp_stanza_t* region_text = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_text(region_text, element->address.region);
+                xmpp_stanza_add_child(region, region_text);
+                xmpp_stanza_release(region_text);
+
+                xmpp_stanza_add_child(address, region);
+                xmpp_stanza_release(region);
+            }
+            if (element->address.pcode) {
+                xmpp_stanza_t* pcode = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(pcode, "PCODE");
+
+                xmpp_stanza_t* pcode_text = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_text(pcode_text, element->address.pcode);
+                xmpp_stanza_add_child(pcode, pcode_text);
+                xmpp_stanza_release(pcode_text);
+
+                xmpp_stanza_add_child(address, pcode);
+                xmpp_stanza_release(pcode);
+            }
+            if (element->address.country) {
+                xmpp_stanza_t* ctry = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(ctry, "CTRY");
+
+                xmpp_stanza_t* ctry_text = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_text(ctry_text, element->address.country);
+                xmpp_stanza_add_child(ctry, ctry_text);
+                xmpp_stanza_release(ctry_text);
+
+                xmpp_stanza_add_child(address, ctry);
+                xmpp_stanza_release(ctry);
+            }
+
+            xmpp_stanza_add_child(vcard_stanza, address);
+            xmpp_stanza_release(address);
+            break;
+        }
+        case VCARD_TELEPHONE:
+        {
+            // <TEL> element
+            xmpp_stanza_t* tel = xmpp_stanza_new(ctx);
+            xmpp_stanza_set_name(tel, "TEL");
+
+            // Options
+            if (element->telephone.options & VCARD_HOME) {
+                xmpp_stanza_t* home = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(home, "HOME");
+
+                xmpp_stanza_add_child(tel, home);
+                xmpp_stanza_release(home);
+            }
+            if ((element->telephone.options & VCARD_WORK) == VCARD_WORK) {
+                xmpp_stanza_t* work = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(work, "WORK");
+
+                xmpp_stanza_add_child(tel, work);
+                xmpp_stanza_release(work);
+            }
+            if ((element->telephone.options & VCARD_TEL_VOICE) == VCARD_TEL_VOICE) {
+                xmpp_stanza_t* voice = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(voice, "VOICE");
+
+                xmpp_stanza_add_child(tel, voice);
+                xmpp_stanza_release(voice);
+            }
+            if ((element->telephone.options & VCARD_TEL_FAX) == VCARD_TEL_FAX) {
+                xmpp_stanza_t* fax = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(fax, "FAX");
+
+                xmpp_stanza_add_child(tel, fax);
+                xmpp_stanza_release(fax);
+            }
+            if ((element->telephone.options & VCARD_TEL_PAGER) == VCARD_TEL_PAGER) {
+                xmpp_stanza_t* pager = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(pager, "PAGER");
+
+                xmpp_stanza_add_child(tel, pager);
+                xmpp_stanza_release(pager);
+            }
+            if ((element->telephone.options & VCARD_TEL_MSG) == VCARD_TEL_MSG) {
+                xmpp_stanza_t* msg = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(msg, "MSG");
+
+                xmpp_stanza_add_child(tel, msg);
+                xmpp_stanza_release(msg);
+            }
+            if ((element->telephone.options & VCARD_TEL_CELL) == VCARD_TEL_CELL) {
+                xmpp_stanza_t* cell = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(cell, "CELL");
+
+                xmpp_stanza_add_child(tel, cell);
+                xmpp_stanza_release(cell);
+            }
+            if ((element->telephone.options & VCARD_TEL_VIDEO) == VCARD_TEL_VIDEO) {
+                xmpp_stanza_t* video = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(video, "VIDEO");
+
+                xmpp_stanza_add_child(tel, video);
+                xmpp_stanza_release(video);
+            }
+            if ((element->telephone.options & VCARD_TEL_BBS) == VCARD_TEL_BBS) {
+                xmpp_stanza_t* bbs = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(bbs, "BBS");
+
+                xmpp_stanza_add_child(tel, bbs);
+                xmpp_stanza_release(bbs);
+            }
+            if ((element->telephone.options & VCARD_TEL_MODEM) == VCARD_TEL_MODEM) {
+                xmpp_stanza_t* modem = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(modem, "MODEM");
+
+                xmpp_stanza_add_child(tel, modem);
+                xmpp_stanza_release(modem);
+            }
+            if ((element->telephone.options & VCARD_TEL_ISDN) == VCARD_TEL_ISDN) {
+                xmpp_stanza_t* isdn = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(isdn, "ISDN");
+
+                xmpp_stanza_add_child(tel, isdn);
+                xmpp_stanza_release(isdn);
+            }
+            if ((element->telephone.options & VCARD_TEL_PCS) == VCARD_TEL_PCS) {
+                xmpp_stanza_t* pcs = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(pcs, "PCS");
+
+                xmpp_stanza_add_child(tel, pcs);
+                xmpp_stanza_release(pcs);
+            }
+            if ((element->telephone.options & VCARD_PREF) == VCARD_PREF) {
+                xmpp_stanza_t* pref = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(pref, "PREF");
+
+                xmpp_stanza_add_child(tel, pref);
+                xmpp_stanza_release(pref);
+            }
+
+            if (element->telephone.number) {
+                xmpp_stanza_t* number = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(number, "NUMBER");
+
+                xmpp_stanza_t* number_text = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_text(number_text, element->telephone.number);
+                xmpp_stanza_add_child(number, number_text);
+                xmpp_stanza_release(number_text);
+
+                xmpp_stanza_add_child(tel, number);
+                xmpp_stanza_release(number);
+            }
+            xmpp_stanza_add_child(vcard_stanza, tel);
+            xmpp_stanza_release(tel);
+            break;
+        }
+        case VCARD_EMAIL:
+        {
+            xmpp_stanza_t* email = xmpp_stanza_new(ctx);
+            xmpp_stanza_set_name(email, "EMAIL");
+
+            if (element->email.options & VCARD_HOME) {
+                xmpp_stanza_t* home = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(home, "HOME");
+
+                xmpp_stanza_add_child(email, home);
+                xmpp_stanza_release(home);
+            }
+            if ((element->email.options & VCARD_WORK) == VCARD_WORK) {
+                xmpp_stanza_t* work = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(work, "WORK");
+
+                xmpp_stanza_add_child(email, work);
+                xmpp_stanza_release(work);
+            }
+            if ((element->email.options & VCARD_EMAIL_X400) == VCARD_EMAIL_X400) {
+                xmpp_stanza_t* x400 = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(x400, "X400");
+
+                xmpp_stanza_add_child(email, x400);
+                xmpp_stanza_release(x400);
+            }
+            if ((element->email.options & VCARD_PREF) == VCARD_PREF) {
+                xmpp_stanza_t* pref = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(pref, "PREF");
+
+                xmpp_stanza_add_child(email, pref);
+                xmpp_stanza_release(pref);
+            }
+
+            if (element->email.userid) {
+                xmpp_stanza_t* userid = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_name(userid, "USERID");
+
+                xmpp_stanza_t* userid_text = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_text(userid_text, element->email.userid);
+                xmpp_stanza_add_child(userid, userid_text);
+                xmpp_stanza_release(userid_text);
+
+                xmpp_stanza_add_child(email, userid);
+                xmpp_stanza_release(userid);
+            }
+
+            xmpp_stanza_add_child(vcard_stanza, email);
+            xmpp_stanza_release(email);
+            break;
+        }
+        case VCARD_JID:
+        {
+            xmpp_stanza_t* jid = xmpp_stanza_new(ctx);
+            xmpp_stanza_set_name(jid, "JABBERID");
+
+            if (element->jid) {
+                xmpp_stanza_t* jid_text = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_text(jid_text, element->jid);
+                xmpp_stanza_add_child(jid, jid_text);
+                xmpp_stanza_release(jid_text);
+            }
+
+            xmpp_stanza_add_child(vcard_stanza, jid);
+            xmpp_stanza_release(jid);
+            break;
+        }
+        case VCARD_TITLE:
+        {
+            xmpp_stanza_t* title = xmpp_stanza_new(ctx);
+            xmpp_stanza_set_name(title, "TITLE");
+
+            if (element->title) {
+                xmpp_stanza_t* title_text = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_text(title_text, element->title);
+                xmpp_stanza_add_child(title, title_text);
+                xmpp_stanza_release(title_text);
+            }
+
+            xmpp_stanza_add_child(vcard_stanza, title);
+            xmpp_stanza_release(title);
+            break;
+        }
+        case VCARD_ROLE:
+        {
+            xmpp_stanza_t* role = xmpp_stanza_new(ctx);
+            xmpp_stanza_set_name(role, "ROLE");
+
+            if (element->role) {
+                xmpp_stanza_t* role_text = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_text(role_text, element->role);
+                xmpp_stanza_add_child(role, role_text);
+                xmpp_stanza_release(role_text);
+            }
+
+            xmpp_stanza_add_child(vcard_stanza, role);
+            xmpp_stanza_release(role);
+            break;
+        }
+        case VCARD_NOTE:
+        {
+            xmpp_stanza_t* note = xmpp_stanza_new(ctx);
+            xmpp_stanza_set_name(note, "NOTE");
+
+            if (element->note) {
+                xmpp_stanza_t* note_text = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_text(note_text, element->note);
+                xmpp_stanza_add_child(note, note_text);
+                xmpp_stanza_release(note_text);
+            }
+
+            xmpp_stanza_add_child(vcard_stanza, note);
+            xmpp_stanza_release(note);
+            break;
+        }
+        case VCARD_URL:
+        {
+            xmpp_stanza_t* url = xmpp_stanza_new(ctx);
+            xmpp_stanza_set_name(url, "URL");
+
+            if (element->url) {
+                xmpp_stanza_t* url_text = xmpp_stanza_new(ctx);
+                xmpp_stanza_set_text(url_text, element->url);
+                xmpp_stanza_add_child(url, url_text);
+                xmpp_stanza_release(url_text);
+            }
+
+            xmpp_stanza_add_child(vcard_stanza, url);
+            xmpp_stanza_release(url);
+            break;
+        }
+        }
+    }
+
+    return vcard_stanza;
+}
+
+static int
+_vcard_print_result(xmpp_stanza_t* const stanza, void* userdata)
+{
+    _userdata* data = (_userdata*)userdata;
+    const char* from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM);
+
+    if (from) {
+        win_println(data->window, THEME_DEFAULT, "!", "vCard for %s", from);
+    } else {
+        win_println(data->window, THEME_DEFAULT, "!", "This account's vCard");
+    }
+
+    xmpp_stanza_t* vcard_xml = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_VCARD);
+    if (!vcard_parse(vcard_xml, data->vcard)) {
+        return 1;
+    }
+
+    win_show_vcard(data->window, data->vcard);
+
+    return 1;
+}
+
+void
+vcard_print(xmpp_ctx_t* ctx, ProfWin* window, char* jid)
+{
+    if (!jid && vcard_user && vcard_user->modified) {
+        win_println(window, THEME_DEFAULT, "!", "This account's vCard (modified, `/vcard upload` to push)");
+        win_show_vcard(window, vcard_user);
+        return;
+    }
+
+    _userdata* data = calloc(1, sizeof(_userdata));
+    data->vcard = vcard_new();
+    if (!data || !data->vcard) {
+        if (data) {
+            free(data);
+        }
+
+        cons_show("vCard allocation failed");
+        return;
+    }
+
+    data->window = window;
+
+    char* id = connection_create_stanza_id();
+    xmpp_stanza_t* iq = stanza_create_vcard_request_iq(ctx, jid, id);
+
+    iq_id_handler_add(id, _vcard_print_result, (ProfIqFreeCallback)_free_userdata, data);
+
+    free(id);
+    iq_send_stanza(iq);
+    xmpp_stanza_release(iq);
+}
+
+static int
+_vcard_photo_result(xmpp_stanza_t* const stanza, void* userdata)
+{
+    _userdata* data = (_userdata*)userdata;
+    const char* from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM);
+    if (!from) {
+        from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_TO);
+    }
+
+    vcard_element_photo_t* photo = NULL;
+
+    xmpp_stanza_t* vcard_xml = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_VCARD);
+    if (!vcard_parse(vcard_xml, data->vcard)) {
+        return 1;
+    }
+
+    if (data->photo_index < 0) {
+        GList* list_pointer;
+        for (list_pointer = g_queue_peek_head_link(data->vcard->elements); list_pointer != NULL; list_pointer = list_pointer->next) {
+            vcard_element_t* element = list_pointer->data;
+
+            if (element->type != VCARD_PHOTO) {
+                continue;
+            } else {
+                photo = &element->photo;
+            }
+        }
+
+        if (photo == NULL) {
+            cons_show_error("No photo was found in vCard");
+            return 1;
+        }
+    } else {
+        vcard_element_t* element = (vcard_element_t*)g_queue_peek_nth(data->vcard->elements, data->photo_index);
+
+        if (element == NULL) {
+            cons_show_error("No element was found at index %d", data->photo_index);
+            return 1;
+        } else if (element->type != VCARD_PHOTO) {
+            cons_show_error("Element is not a photo");
+            return 1;
+        }
+
+        photo = &element->photo;
+    }
+
+    if (photo->external) {
+        cons_show_error("Cannot handle external value: %s", photo->extval);
+        return 1;
+    }
+
+    GString* filename;
+
+    if (!data->filename) {
+        char* path = files_get_data_path(DIR_PHOTOS);
+        filename = g_string_new(path);
+        free(path);
+        g_string_append(filename, "/");
+
+        errno = 0;
+        int res = g_mkdir_with_parents(filename->str, S_IRWXU);
+        if (res == -1) {
+            const char* errmsg = strerror(errno);
+            if (errmsg) {
+                cons_show_error("Error creating directory %s: %s", filename->str, errmsg);
+                g_string_free(filename, TRUE);
+                return 1;
+            } else {
+                cons_show_error("Unknown error creating directory %s", filename->str);
+                g_string_free(filename, TRUE);
+            }
+        }
+        gchar* from1 = str_replace(from, "@", "_at_");
+        gchar* from2 = str_replace(from1, "/", "_slash_");
+        g_free(from1);
+
+        g_string_append(filename, from2);
+        g_free(from2);
+    } else {
+        filename = g_string_new(data->filename);
+    }
+
+    // check a few image types ourselves
+    // TODO: we could use /etc/mime-types
+    if (g_strcmp0(photo->type, "image/png") == 0 || g_strcmp0(photo->type, "img/png") == 0) {
+        g_string_append(filename, ".png");
+    } else if (g_strcmp0(photo->type, "image/jpeg") == 0 || g_strcmp0(photo->type, "img/jpeg") == 0) {
+        g_string_append(filename, ".jpeg");
+    } else if (g_strcmp0(photo->type, "image/webp") == 0 || g_strcmp0(photo->type, "img/webp") == 0) {
+        g_string_append(filename, ".webp");
+    }
+
+    GError* err = NULL;
+
+    if (g_file_set_contents(filename->str, (gchar*)photo->data, photo->length, &err) == FALSE) {
+        cons_show_error("Unable to save photo: %s", err->message);
+        g_error_free(err);
+        g_string_free(filename, TRUE);
+        return 1;
+    } else {
+        cons_show("Photo saved as %s", filename->str);
+    }
+
+    if (data->open) {
+        gchar** argv;
+        gint argc;
+
+        gchar* cmdtemplate = prefs_get_string(PREF_VCARD_PHOTO_CMD);
+
+        // this makes it work with filenames that contain spaces
+        g_string_prepend(filename, "\"");
+        g_string_append(filename, "\"");
+        gchar* cmd = str_replace(cmdtemplate, "%p", filename->str);
+        g_free(cmdtemplate);
+
+        if (g_shell_parse_argv(cmd, &argc, &argv, &err) == FALSE) {
+            cons_show_error("Failed to parse command template");
+            g_free(cmd);
+        } else {
+            if (!call_external(argv)) {
+                cons_show_error("Unable to execute command");
+            }
+            g_strfreev(argv);
+        }
+    }
+
+    g_string_free(filename, TRUE);
+
+    return 1;
+}
+
+void
+vcard_photo(xmpp_ctx_t* ctx, char* jid, char* filename, int index, gboolean open)
+{
+    _userdata* data = calloc(1, sizeof(_userdata));
+    data->vcard = vcard_new();
+
+    if (!data || !data->vcard) {
+        if (data) {
+            free(data);
+        }
+
+        cons_show("vCard allocation failed");
+        return;
+    }
+
+    data->photo_index = index;
+    data->open = open;
+
+    if (filename) {
+        data->filename = strdup(filename);
+    }
+
+    char* id = connection_create_stanza_id();
+    xmpp_stanza_t* iq = stanza_create_vcard_request_iq(ctx, jid, id);
+
+    iq_id_handler_add(id, _vcard_photo_result, (ProfIqFreeCallback)_free_userdata, data);
+
+    free(id);
+    iq_send_stanza(iq);
+    xmpp_stanza_release(iq);
+}
+
+static int
+_vcard_refresh_result(xmpp_stanza_t* const stanza, void* userdata)
+{
+    xmpp_stanza_t* vcard_xml = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_VCARD);
+
+    vcard_free_full(vcard_user);
+    if (!vcard_parse(vcard_xml, vcard_user)) {
+        return 1;
+    }
+
+    cons_show("vCard refreshed");
+    vcard_user->modified = FALSE;
+    return 1;
+}
+
+void
+vcard_user_refresh(void)
+{
+    if (!vcard_user) {
+        vcard_user = vcard_new();
+    }
+
+    if (!vcard_user) {
+        return;
+    }
+
+    char* id = connection_create_stanza_id();
+    xmpp_stanza_t* iq = stanza_create_vcard_request_iq(connection_get_ctx(), NULL, id);
+
+    iq_id_handler_add(id, _vcard_refresh_result, NULL, NULL);
+
+    free(id);
+    iq_send_stanza(iq);
+    xmpp_stanza_release(iq);
+}
+
+// Upload a vCard and set it as the currently connected account's vCard
+void
+vcard_upload(xmpp_ctx_t* ctx, vCard* vcard)
+{
+    char* id = connection_create_stanza_id();
+    xmpp_stanza_t* iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id);
+    xmpp_stanza_set_from(iq, connection_get_fulljid());
+
+    xmpp_stanza_t* vcard_stanza = vcard_to_xml(ctx, vcard);
+
+    if (!vcard_stanza) {
+        xmpp_stanza_release(iq);
+        free(id);
+        return;
+    }
+
+    xmpp_stanza_add_child(iq, vcard_stanza);
+    xmpp_stanza_release(vcard_stanza);
+
+    free(id);
+    iq_send_stanza(iq);
+    xmpp_stanza_release(iq);
+}
+
+void
+vcard_user_save(void)
+{
+    vcard_upload(connection_get_ctx(), vcard_user);
+    vcard_user->modified = FALSE;
+}
+
+void
+vcard_user_set_fullname(char* fullname)
+{
+    if (vcard_user->fullname) {
+        free(vcard_user->fullname);
+    }
+    if (fullname) {
+        vcard_user->fullname = strdup(fullname);
+    } else {
+        vcard_user->fullname = NULL;
+    }
+
+    vcard_user->modified = TRUE;
+}
+
+void
+vcard_user_set_name_family(char* family)
+{
+    if (vcard_user->name.family) {
+        free(vcard_user->name.family);
+    }
+    if (family) {
+        vcard_user->name.family = strdup(family);
+    } else {
+        vcard_user->name.family = NULL;
+    }
+
+    vcard_user->modified = TRUE;
+}
+
+void
+vcard_user_set_name_given(char* given)
+{
+    if (vcard_user->name.given) {
+        free(vcard_user->name.given);
+    }
+    if (given) {
+        vcard_user->name.given = strdup(given);
+    } else {
+        vcard_user->name.given = NULL;
+    }
+
+    vcard_user->modified = TRUE;
+}
+
+void
+vcard_user_set_name_middle(char* middle)
+{
+    if (vcard_user->name.middle) {
+        free(vcard_user->name.middle);
+    }
+    if (middle) {
+        vcard_user->name.middle = strdup(middle);
+    } else {
+        vcard_user->name.middle = NULL;
+    }
+
+    vcard_user->modified = TRUE;
+}
+
+void
+vcard_user_set_name_prefix(char* prefix)
+{
+    if (vcard_user->name.prefix) {
+        free(vcard_user->name.prefix);
+    }
+    if (prefix) {
+        vcard_user->name.prefix = strdup(prefix);
+    } else {
+        vcard_user->name.prefix = NULL;
+    }
+
+    vcard_user->modified = TRUE;
+}
+
+void
+vcard_user_set_name_suffix(char* suffix)
+{
+    if (vcard_user->name.suffix) {
+        free(vcard_user->name.suffix);
+    }
+    if (suffix) {
+        vcard_user->name.suffix = strdup(suffix);
+    } else {
+        vcard_user->name.suffix = NULL;
+    }
+
+    vcard_user->modified = TRUE;
+}
+
+void
+vcard_user_add_element(vcard_element_t* element)
+{
+    g_queue_push_tail(vcard_user->elements, element);
+    vcard_user->modified = TRUE;
+}
+
+void
+vcard_user_remove_element(unsigned int index)
+{
+    void* pointer = g_queue_pop_nth(vcard_user->elements, index);
+
+    if (pointer) {
+        _free_vcard_element(pointer);
+        vcard_user->modified = TRUE;
+    }
+}
+
+vcard_element_t*
+vcard_user_get_element_index(unsigned int index)
+{
+    return g_queue_peek_nth(vcard_user->elements, index);
+}
+
+ProfWin*
+vcard_user_create_win(void)
+{
+    return wins_new_vcard(vcard_user);
+}
+
+void
+vcard_user_free(void)
+{
+    if (vcard_user) {
+        vcard_free_full(vcard_user);
+        vcard_free(vcard_user);
+    }
+    vcard_user = NULL;
+}
diff --git a/src/xmpp/vcard.h b/src/xmpp/vcard.h
new file mode 100644
index 00000000..9ec04c8c
--- /dev/null
+++ b/src/xmpp/vcard.h
@@ -0,0 +1,171 @@
+/*
+ * vcard.h
+ * vim: expandtab:ts=4:sts=4:sw=4
+ *
+ * Copyright (C) 2022 Marouane L. <techmetx11@disroot.org>
+ *
+ * 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 XMPP_VCARD_H
+#define XMPP_VCARD_H
+
+#include <glib.h>
+
+// 17 bits are currently used, out of (a possible) 32 bits
+typedef enum {
+    VCARD_HOME = 1,
+    VCARD_WORK = 2,
+    VCARD_POSTAL = 4,
+    VCARD_PARCEL = 8,
+    VCARD_INTL = 16,
+    VCARD_PREF = 32,
+    VCARD_TEL_VOICE = 64,
+    VCARD_TEL_FAX = 128,
+    VCARD_TEL_PAGER = 256,
+    VCARD_TEL_MSG = 512,
+    VCARD_TEL_CELL = 1024,
+    VCARD_TEL_VIDEO = 2048,
+    VCARD_TEL_BBS = 4096,
+    VCARD_TEL_MODEM = 8192,
+    VCARD_TEL_ISDN = 16384,
+    VCARD_TEL_PCS = 32768,
+    VCARD_EMAIL_X400 = 65536,
+    VCARD_EMAIL_INTERNET = 131072,
+    VCARD_DOM = 262144
+} vcard_element_options_t;
+
+typedef struct _vcard_name
+{
+    char *family, *given, *middle, *prefix, *suffix;
+} vcard_name_t;
+
+typedef struct _vcard_element_photo
+{
+    union {
+        struct
+        {
+            guchar* data;
+            char* type;
+            gsize length;
+        };
+        char* extval;
+    };
+    gboolean external;
+} vcard_element_photo_t;
+
+typedef struct _vcard_element_address
+{
+    char *pobox, *extaddr, *street, *locality, *region, *pcode, *country;
+
+    // Options used:
+    // VCARD_HOME
+    // VCARD_WORK
+    // VCARD_POSTAL
+    // VCARD_PARCEL
+    // VCARD_DOM
+    // VCARD_INTL
+    // VCARD_PREF
+    vcard_element_options_t options;
+} vcard_element_address_t;
+
+typedef struct _vcard_element_telephone
+{
+    char* number;
+
+    // Options used:
+    // VCARD_HOME
+    // VCARD_WORK
+    // VCARD_TEL_VOICE
+    // VCARD_TEL_FAX
+    // VCARD_TEL_PAGER
+    // VCARD_TEL_MSG
+    // VCARD_TEL_CELL
+    // VCARD_TEL_VIDEO
+    // VCARD_TEL_BBS
+    // VCARD_TEL_MODEM
+    // VCARD_TEL_ISDN
+    // VCARD_TEL_PCS
+    // VCARD_PREF
+    vcard_element_options_t options;
+} vcard_element_telephone_t;
+
+typedef struct _vcard_element_email
+{
+    char* userid;
+
+    // Options used:
+    // VCARD_HOME
+    // VCARD_WORK
+    // VCARD_EMAIL_X400
+    // VCARD_PREF
+    vcard_element_options_t options;
+} vcard_element_email_t;
+
+typedef enum _vcard_element_type {
+    VCARD_NICKNAME,
+    VCARD_PHOTO,
+    VCARD_BIRTHDAY,
+    VCARD_ADDRESS,
+    VCARD_TELEPHONE,
+    VCARD_EMAIL,
+    VCARD_JID,
+    VCARD_TITLE,
+    VCARD_ROLE,
+    VCARD_NOTE,
+    VCARD_URL
+} vcard_element_type;
+
+typedef struct _vcard_element
+{
+    vcard_element_type type;
+
+    union {
+        char *nickname, *jid, *title, *role, *note, *url;
+        vcard_element_photo_t photo;
+        GDateTime* birthday;
+        vcard_element_address_t address;
+        vcard_element_telephone_t telephone;
+        vcard_element_email_t email;
+    };
+} vcard_element_t;
+
+typedef struct _vcard
+{
+    // These elements are only meant to appear once (per DTD)
+    vcard_name_t name;
+    char* fullname;
+
+    gboolean modified;
+
+    // GQueue of vcard_element*
+    GQueue* elements;
+} vCard;
+
+#endif
diff --git a/src/xmpp/vcard_funcs.h b/src/xmpp/vcard_funcs.h
new file mode 100644
index 00000000..5f4aa3d0
--- /dev/null
+++ b/src/xmpp/vcard_funcs.h
@@ -0,0 +1,66 @@
+/*
+ * vcard_funcs.h
+ * vim: expandtab:ts=4:sts=4:sw=4
+ *
+ * Copyright (C) 2022 Marouane L. <techmetx11@disroot.org>
+ *
+ * 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 XMPP_VCARD_FUNCS_H
+#define XMPP_VCARD_FUNCS_H
+
+#include "ui/win_types.h"
+#include "xmpp/vcard.h"
+
+vCard* vcard_new();
+void vcard_free(vCard* vcard);
+void vcard_free_full(vCard* vcard);
+
+gboolean vcard_parse(xmpp_stanza_t* vcard_xml, vCard* vcard);
+
+void vcard_print(xmpp_ctx_t* ctx, ProfWin* window, char* jid);
+void vcard_photo(xmpp_ctx_t* ctx, char* jid, char* filename, int index, gboolean open);
+
+void vcard_user_refresh(void);
+void vcard_user_save(void);
+void vcard_user_set_fullname(char* fullname);
+void vcard_user_set_name_family(char* family);
+void vcard_user_set_name_given(char* given);
+void vcard_user_set_name_middle(char* middle);
+void vcard_user_set_name_prefix(char* prefix);
+void vcard_user_set_name_suffix(char* suffix);
+
+void vcard_user_add_element(vcard_element_t* element);
+void vcard_user_remove_element(unsigned int index);
+vcard_element_t* vcard_user_get_element_index(unsigned int index);
+ProfWin* vcard_user_create_win();
+
+void vcard_user_free(void);
+#endif
diff --git a/tests/unittests/ui/stub_ui.c b/tests/unittests/ui/stub_ui.c
index ee759bb2..f95ee0d8 100644
--- a/tests/unittests/ui/stub_ui.c
+++ b/tests/unittests/ui/stub_ui.c
@@ -1256,6 +1256,11 @@ win_create_plugin(const char* const plugin_name, const char* const tag)
 {
     return NULL;
 }
+ProfWin*
+win_create_vcard(vCard* vcard)
+{
+    return NULL;
+}
 
 char*
 win_get_tab_identifier(ProfWin* window)
diff --git a/tests/unittests/ui/stub_vcardwin.c b/tests/unittests/ui/stub_vcardwin.c
new file mode 100644
index 00000000..e73eb04b
--- /dev/null
+++ b/tests/unittests/ui/stub_vcardwin.c
@@ -0,0 +1,4 @@
+void
+vcardwin_update(void)
+{
+}
diff --git a/tests/unittests/xmpp/stub_vcard.c b/tests/unittests/xmpp/stub_vcard.c
new file mode 100644
index 00000000..cc48f92c
--- /dev/null
+++ b/tests/unittests/xmpp/stub_vcard.c
@@ -0,0 +1,79 @@
+#include "ui/ui.h"
+#include "xmpp/vcard.h"
+
+ProfWin*
+vcard_user_create_win()
+{
+    return NULL;
+}
+
+void
+vcard_user_add_element(vcard_element_t* element)
+{
+}
+
+void
+vcard_user_remove_element(unsigned int index)
+{
+}
+
+void
+vcard_print(xmpp_ctx_t* ctx, ProfWin* window, char* jid)
+{
+}
+
+void
+vcard_photo(xmpp_ctx_t* ctx, char* jid, char* filename, int index, gboolean open)
+{
+}
+
+void
+vcard_user_refresh(void)
+{
+}
+
+void
+vcard_user_save(void)
+{
+}
+
+void
+vcard_user_set_fullname(char* fullname)
+{
+}
+
+void
+vcard_user_set_name_family(char* family)
+{
+}
+
+void
+vcard_user_set_name_given(char* given)
+{
+}
+
+void
+vcard_user_set_name_middle(char* middle)
+{
+}
+
+void
+vcard_user_set_name_prefix(char* prefix)
+{
+}
+
+void
+vcard_user_set_name_suffix(char* suffix)
+{
+}
+
+vcard_element_t*
+vcard_user_get_element_index(unsigned int index)
+{
+    return NULL;
+}
+
+void
+vcard_user_free(void)
+{
+}