diff --git a/src/sbbs3/mailsrvr.c b/src/sbbs3/mailsrvr.c
index 00b9683cab38fe904cfb1b60aab93850256e097d..5bde6041fd811c74d779e516c9582cbde43ce3a0 100644
--- a/src/sbbs3/mailsrvr.c
+++ b/src/sbbs3/mailsrvr.c
@@ -2715,44 +2715,6 @@ static void parse_mail_address(char* p
 	strip_char(name, name, '\\');
 }
 
-/* Decode quoted-printable content-transfer-encoded text */
-/* Ignores (strips) unsupported ctrl chars and non-ASCII chars */
-/* Does not enforce 76 char line length limit */
-static char* qp_decode(char* buf)
-{
-	uchar*	p=(uchar*)buf;
-	uchar*	dest=p;
-
-	for(;;p++) {
-		if(*p==0) {
-			*dest++='\r';
-			*dest++='\n';
-			break;
-		}
-		if(*p==' ' || (*p>='!' && *p<='~' && *p!='=') || *p=='\t')
-			*dest++=*p;
-		else if(*p=='=') {
-			p++;
-			if(*p==0) 	/* soft link break */
-				break;
-			if(IS_HEXDIGIT(*p) && IS_HEXDIGIT(*(p+1))) {
-				char hex[3];
-				hex[0]=*p;
-				hex[1]=*(p+1);
-				hex[2]=0;
-				/* ToDo: what about encoded NULs and the like? */
-				*dest++=(uchar)strtoul(hex,NULL,16);
-				p++;
-			} else {	/* bad encoding */
-				*dest++='=';
-				*dest++=*p;
-			}
-		}
-	}
-	*dest=0;
-	return buf;
-}
-
 static BOOL checktag(scfg_t *scfg, char *tag, uint usernum)
 {
 	char	fname[MAX_PATH+1];
@@ -2950,12 +2912,6 @@ static void smtp_thread(void* arg)
 
 	} cmd = SMTP_CMD_NONE;
 
-	enum {
-			 ENCODING_NONE
-			,ENCODING_BASE64
-			,ENCODING_QUOTED_PRINTABLE
-	} content_encoding = ENCODING_NONE;
-
 	SetThreadName("sbbs/smtp");
 	thread_up(TRUE /* setuid */);
 
@@ -4059,24 +4015,7 @@ static void smtp_thread(void* arg)
 				p=buf;
 				if(*p=='.') p++;	/* Transparency (RFC821 4.5.2) */
 				if(msgtxt!=NULL) {
-					switch(content_encoding) {
-						case ENCODING_BASE64:
-							{
-								char	decode_buf[sizeof(buf)];
-
-								if(b64_decode(decode_buf, sizeof(decode_buf), p, strlen(p))<0)
-									fprintf(msgtxt,"\r\n!Base64 decode error: %s\r\n", p);
-								else
-									fputs(decode_buf, msgtxt);
-							}
-							break;
-						case ENCODING_QUOTED_PRINTABLE:
-							fputs(qp_decode(p), msgtxt);
-							break;
-						default:
-							fprintf(msgtxt, "%s\r\n", p);
-							break;
-					}
+					fputs(p, msgtxt);
 				}
 				lines++;
 				/* release time-slices every x lines */
@@ -4102,20 +4041,6 @@ static void smtp_thread(void* arg)
 							,sender,		sizeof(sender)-1
 							,sender_addr,	sizeof(sender_addr)-1);
 					}
-					else if(stricmp(field,"CONTENT-TRANSFER-ENCODING")==0) {
-						lprintf(LOG_INFO,"%04d %s %s %s = %s", socket, client.protocol, client_id, field, p);
-						if(stricmp(p,"base64")==0)
-							content_encoding=ENCODING_BASE64;
-						else if(stricmp(p,"quoted-printable")==0)
-							content_encoding=ENCODING_QUOTED_PRINTABLE;
-						else {	/* Other (e.g. 7bit, 8bit, binary) */
-							content_encoding=ENCODING_NONE;
-							if(msgtxt!=NULL) 
-								fprintf(msgtxt, "%s\r\n", buf);
-						}
-						hdr_lines++;
-						continue;
-					}
 				}
 			}
 
@@ -4410,7 +4335,6 @@ static void smtp_thread(void* arg)
 				break;
 			}
 			rcpt_count=0;
-			content_encoding=ENCODING_NONE;
 
 			memset(mailproc_to_match,FALSE,sizeof(BOOL)*mailproc_count);
 
@@ -4468,7 +4392,6 @@ static void smtp_thread(void* arg)
 				break;
 			}
 			rcpt_count=0;
-			content_encoding=ENCODING_NONE;
 			memset(mailproc_to_match,FALSE,sizeof(BOOL)*mailproc_count);
 			sockprintf(socket,client.protocol,session,ok_rsp);
 			badcmds=0;
diff --git a/src/smblib/smbdefs.h b/src/smblib/smbdefs.h
index 89d195f53dfbf80f672839e719a16b7f6c8b6bcc..36e7797d958427ffe467922552b1cdaed7b9ee0e 100644
--- a/src/smblib/smbdefs.h
+++ b/src/smblib/smbdefs.h
@@ -558,6 +558,7 @@ typedef struct {				/* Message */
 	char*		editor;			/* Message editor (if known) */
 	char*		mime_version;	/* MIME Version (if applicable) */
 	char*		content_type;	/* MIME Content-Type (if applicable) */
+	char*		content_encoding; /* MIME Content-Transfer-Encoding (if applicable) */
 	char*		text_charset;	/* MIME text <charset>  (if applicable) - malloc'd */
 	char*		text_subtype;	/* MIME text/<sub-type> (if applicable) - malloc'd */
 	uint16_t	to_agent,		/* Type of agent message is to */
diff --git a/src/smblib/smblib.c b/src/smblib/smblib.c
index a670a1262acaa013c1b415d88ddaa55317d2e63b..d0ce6c6334d02a5c72e9123d6e268355a1529753 100644
--- a/src/smblib/smblib.c
+++ b/src/smblib/smblib.c
@@ -859,6 +859,12 @@ static void set_convenience_ptr(smbmsg_t* msg, uint16_t hfield_type, void* hfiel
 				smb_parse_content_type(p, &(msg->text_subtype), &(msg->text_charset));
 				break;
 			}
+			if(strnicmp(p, "Content-Transfer-Encoding:", 26) == 0) {
+				p += 26;
+				SKIP_WHITESPACE(p);
+				msg->content_encoding = p;
+				break;
+			}
 			break;
 		}
 	}
@@ -897,6 +903,7 @@ static void clear_convenience_ptrs(smbmsg_t* msg)
 	msg->newsgroups=NULL;
 	msg->mime_version=NULL;
 	msg->content_type=NULL;
+	msg->content_encoding=NULL;
 	msg->text_subtype=NULL;
 	msg->text_charset=NULL;
 
diff --git a/src/smblib/smbtxt.c b/src/smblib/smbtxt.c
index 0ffce0d1d5320d54dda792feee3625b5782544da..4d67dcec76c24c4d4793735cf009fa58e9e37ba7 100644
--- a/src/smblib/smbtxt.c
+++ b/src/smblib/smbtxt.c
@@ -1,7 +1,5 @@
 /* Synchronet message base (SMB) message text library routines */
 
-/* $Id: smbtxt.c,v 1.49 2019/11/19 22:04:55 rswindell Exp $ */
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
@@ -15,21 +13,9 @@
  * See the GNU Lesser General Public License for more details: lgpl.txt or	*
  * http://www.fsf.org/copyleft/lesser.html									*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -42,7 +28,7 @@
 #include "base64.h"
 #include "lzh.h"
 
-char* SMBCALL smb_getmsgtxt(smb_t* smb, smbmsg_t* msg, ulong mode)
+char* smb_getmsgtxt(smb_t* smb, smbmsg_t* msg, ulong mode)
 {
 	char*	buf;
 	char*	preamble;
@@ -217,7 +203,7 @@ char* SMBCALL smb_getmsgtxt(smb_t* smb, smbmsg_t* msg, ulong mode)
 	return(buf);
 }
 
-void SMBCALL smb_freemsgtxt(char* buf)
+void smb_freemsgtxt(char* buf)
 {
 	if(buf!=NULL)
 		free(buf);
@@ -275,6 +261,22 @@ static size_t strStartsWith_i(const char* buf, const char* match)
 	return 0;
 }
 
+static enum content_transfer_encoding mime_encoding(const char* str)
+{
+	const char* p = str;
+
+	if(str ==  NULL)
+		return CONTENT_TRANFER_ENCODING_NONE;
+	SKIP_WHITESPACE(p);
+	if(strnicmp(p, "base64", 6) == 0)
+		return CONTENT_TRANFER_ENCODING_BASE64;
+	if(strnicmp(p, "quoted-printable", 16) == 0)
+		return CONTENT_TRANFER_ENCODING_QUOTED_PRINTABLE;
+	if(strnicmp(p, "7bit", 4) == 0 || strnicmp(p, "8bit", 4) == 0 || strnicmp(p, "binary", 6) == 0)
+		return CONTENT_TRANFER_ENCODING_NONE;
+	return CONTENT_TRANFER_ENCODING_OTHER;
+}
+
 static enum content_transfer_encoding mime_getxferencoding(const char* beg, const char* end)
 {
 	const char* p = beg;
@@ -286,15 +288,7 @@ static enum content_transfer_encoding mime_getxferencoding(const char* beg, cons
 			FIND_CHAR(p, '\n');
 			continue;
 		}
-		p += len;
-		SKIP_WHITESPACE(p);
-		if(strnicmp(p, "base64", 6) == 0)
-			return CONTENT_TRANFER_ENCODING_BASE64;
-		if(strnicmp(p, "quoted-printable", 16) == 0)
-			return CONTENT_TRANFER_ENCODING_QUOTED_PRINTABLE;
-		if(strnicmp(p, "7bit", 4) == 0 || strnicmp(p, "8bit", 4) == 0 || strnicmp(p, "binary", 6) == 0)
-			return CONTENT_TRANFER_ENCODING_NONE;
-		return CONTENT_TRANFER_ENCODING_OTHER;
+		return mime_encoding(p + len);
 	}
 
 	return CONTENT_TRANFER_ENCODING_NONE;
@@ -354,7 +348,7 @@ static BOOL mime_getattachment(const char* beg, const char* end, char* attachmen
 }
 
 // Parses a MIME text/* content-type header field
-void SMBCALL smb_parse_content_type(const char* content_type, char** subtype, char** charset)
+void smb_parse_content_type(const char* content_type, char** subtype, char** charset)
 {
 	if(subtype != NULL) {
 		FREE_AND_NULL(*subtype);
@@ -405,8 +399,8 @@ void SMBCALL smb_parse_content_type(const char* content_type, char** subtype, ch
 	}
 }
 
-/* Find the specified content-type in a multi-pat MIME-encoded message body, recursively */
-static const char* mime_getcontent(const char* buf, const char* content_type, const char* content_match
+/* Find the specified content-type in a multi-part MIME-encoded message body, recursively */
+static const char* mime_getpart(const char* buf, const char* content_type, const char* content_match
 	,int depth, enum content_transfer_encoding* encoding, char** charset, char* attachment, size_t attachment_len, int index)
 {
 	const char*	txt;
@@ -465,12 +459,12 @@ static const char* mime_getcontent(const char* buf, const char* content_type, co
 		const char* cp;
 		if((match_len && strnicmp(content_type, match1, match_len) && strnicmp(content_type, match2, match_len))
 			|| (attachment != NULL && !mime_getattachment(txt, p, attachment, attachment_len))) {
-			if((cp = mime_getcontent(p, content_type, content_match, depth + 1, encoding, charset, attachment, attachment_len, index)) != NULL)
+			if((cp = mime_getpart(p, content_type, content_match, depth + 1, encoding, charset, attachment, attachment_len, index)) != NULL)
 				return cp;
 			continue;
 		}
 		if(found++ != index) {
-			if((cp = mime_getcontent(p, content_type, content_match, depth + 1, encoding, charset, attachment, attachment_len, index)) != NULL)
+			if((cp = mime_getpart(p, content_type, content_match, depth + 1, encoding, charset, attachment, attachment_len, index)) != NULL)
 				return cp;
 			continue;
 		}
@@ -490,29 +484,41 @@ static const char* mime_getcontent(const char* buf, const char* content_type, co
 
 /* Get just the (first) plain-text or HTML portion of a MIME-encoded multi-part message body */
 /* Returns NULL if there is no MIME-encoded plain-text/html portion of the message */
-char* SMBCALL smb_getplaintext(smbmsg_t* msg, char* buf)
+char* smb_getplaintext(smbmsg_t* msg, char* buf)
 {
+	size_t len;
 	const char*	txt;
 	enum content_transfer_encoding xfer_encoding = CONTENT_TRANFER_ENCODING_NONE;
 
 	if(msg->mime_version == NULL || msg->content_type == NULL)	/* not MIME */
 		return NULL;
-	txt = mime_getcontent(buf, msg->content_type, "text/plain", 0, &xfer_encoding, &msg->text_charset
-		,/* attachment: */NULL, /* attachment_len: */0, /* index: */0);
-	if(txt == NULL) {
-		txt = mime_getcontent(buf, msg->content_type, "text/html", 0, &xfer_encoding, &msg->text_charset
+	if(strStartsWith_i(msg->content_type, "multipart/") > 0) {
+		txt = mime_getpart(buf, msg->content_type, "text/plain", 0, &xfer_encoding, &msg->text_charset
 			,/* attachment: */NULL, /* attachment_len: */0, /* index: */0);
-		if(txt == NULL)
-			return NULL;
-		free(msg->text_subtype);
-		msg->text_subtype = strdup("html");
+		if(txt == NULL) {
+			txt = mime_getpart(buf, msg->content_type, "text/html", 0, &xfer_encoding, &msg->text_charset
+				,/* attachment: */NULL, /* attachment_len: */0, /* index: */0);
+			if(txt == NULL)
+				return NULL;
+			free(msg->text_subtype);
+			msg->text_subtype = strdup("html");
+		} else {
+			free(msg->text_subtype);
+			msg->text_subtype = strdup("plain");
+		}
+
+		len = strlen(txt);
+		memmove(buf, txt, len + 1);
 	} else {
-		free(msg->text_subtype);
-		msg->text_subtype = strdup("plain");
+		/* Single-part MIME */
+		if(strStartsWith_i(msg->content_type, "text/") < 1) {
+			*buf = '\0';
+			return buf;
+		}
+		xfer_encoding = mime_encoding(msg->content_encoding);
+		len = strlen(buf);
 	}
-
-	memmove(buf, txt, strlen(txt)+1);
-	if(*buf == 0)	/* No decoding necessary */
+	if(len == 0)	/* No decoding necessary */
 		return buf;
 	if(xfer_encoding == CONTENT_TRANFER_ENCODING_QUOTED_PRINTABLE)
 		qp_decode(buf);
@@ -520,41 +526,75 @@ char* SMBCALL smb_getplaintext(smbmsg_t* msg, char* buf)
 		char* decoded = strdup(buf);
 		if(decoded == NULL)
 			return NULL;
-		if(b64_decode(decoded, strlen(decoded), buf, strlen(buf)) > 0)
+		if(b64_decode(decoded, len, buf, len) > 0)
 			strcpy(buf, decoded);
 		free(decoded);
 	}
-
 	return buf;
 }
 
-/* Get just a base64-encoded attachment (just one) from MIME-encoded message body */
-/* This function is destructive (over-writes 'buf' with decoded attachment)! */
-uint8_t* SMBCALL smb_getattachment(smbmsg_t* msg, char* buf, char* filename, size_t filename_len, uint32_t* filelen, int index)
+/* Get an attachment (just one) from single-part or multi-part MIME-encoded message body */
+/* This function can be destructive (over-write 'buf' with decoded attachment)! */
+uint8_t* smb_getattachment(smbmsg_t* msg, char* buf, char* filename, size_t filename_len, uint32_t* filelen, int index)
 {
 	const char*	txt;
 	enum content_transfer_encoding xfer_encoding = CONTENT_TRANFER_ENCODING_NONE;
 
 	if(msg->mime_version == NULL || msg->content_type == NULL)	/* not MIME */
 		return NULL;
-	txt = mime_getcontent(buf, msg->content_type, /* match-type: */NULL, 0, &xfer_encoding, /* charset: */NULL
-		,/* attachment: */filename, filename_len, index);
-	if(txt != NULL && xfer_encoding == CONTENT_TRANFER_ENCODING_BASE64) {
-		memmove(buf, txt, strlen(txt)+1);
-		int result = b64_decode(buf, strlen(buf), buf, strlen(buf));
+	if(strStartsWith_i(msg->content_type, "multipart/") > 0) {
+		txt = mime_getpart(buf, msg->content_type, /* match-type: */NULL, 0, &xfer_encoding, /* charset: */NULL
+			,/* attachment: */filename, filename_len, index);
+		if(txt != NULL && xfer_encoding == CONTENT_TRANFER_ENCODING_BASE64) {
+			size_t len = strlen(txt);
+			memmove(buf, txt, len + 1);
+			int result = b64_decode(buf, len, buf, len);
+			if(result < 1)
+				return NULL;
+			if(filelen != NULL)
+				*filelen = result;
+			return (uint8_t*)buf;
+		}
+		return NULL;	/* No attachment */
+	}
+	/* Single-part MIME */
+	if(index > 0)
+		return NULL;
+	if(strStartsWith_i(msg->content_type, "text/") > 0)
+		return NULL;
+	if(filename != NULL) {
+		char* fname = strstr(msg->content_type, "name=");
+		if(fname == NULL)
+			return NULL;
+		fname += 5;
+		char* term = NULL;
+		if(*fname == '"') {
+			fname++;
+			term = strchr(fname, '"');
+		} else
+			term = strchr(fname, ';');
+		if(term != NULL)
+			*term = '\0';
+		strncpy(filename, fname, filename_len);
+		filename[filename_len - 1] = '\0';
+	}
+	if(mime_encoding(msg->content_encoding) == CONTENT_TRANFER_ENCODING_BASE64) {
+		size_t len = strlen(buf);
+		int result = b64_decode(buf, len, buf, len);
 		if(result < 1)
 			return NULL;
 		if(filelen != NULL)
 			*filelen = result;
-		return (uint8_t*)buf;
+	} else {
+		if(filelen != NULL)
+			*filelen = strlen(buf);
 	}
-
-	return NULL;	/* No attachment */
+	return buf;
 }
 
 /* Return number of file attachments contained in MIME-encoded message body */
 /* 'body' may be NULL if the body text is not already read/available */
-ulong SMBCALL smb_countattachments(smb_t* smb, smbmsg_t* msg, const char* body)
+ulong smb_countattachments(smb_t* smb, smbmsg_t* msg, const char* body)
 {
 	if(msg->mime_version == NULL || msg->content_type == NULL)	/* not MIME */
 		return 0;