Browse Source

Basic RODEX support (#2186)

Adds basic support for the new mail UI RODEX.

Fixes #1567

Thanks to @RagnarokNova, @Atemo, @aleos89 and everyone else that helped me with this.
Additionally I would like to thank @jezznar and @hazimjauhari90 for their good testing in the pull request.
Lemongrass3110 8 năm trước cách đây
mục cha
commit
58776da1ac

+ 23 - 0
conf/battle/misc.conf

@@ -134,6 +134,29 @@ cashshop_show_points: no
 // 2 = Yes, when there are unread mails
 mail_show_status: 0
 
+// Amount of mails a user can send a day.
+// Default: 100
+// 0 = Unlimited
+mail_daily_count: 100
+
+// Fee for transferring zeny via mail (Note 2)
+// NOTE: this rate is hardcoded in the client, you need to diff your client accordingly if you change this value.
+// Default: 2(2%)
+// 0 = No fee
+mail_zeny_fee: 2
+
+// Amount of zeny to pay for each attached item
+// NOTE: this fee is hardcoded in the client, you need to diff your client accordingly if you change this value.
+// Default: 2500
+// 0 = No fee
+mail_attachment_price: 2500
+
+// Maximum weight in total that can be attached to a mail
+// NOTE: this limit is hardcoded in the client, you need to diff your client accordingly if you change this value.
+// Default: 2000
+// 0 = No weight limit
+mail_attachment_weight: 2000
+
 // Is monster transformation disabled during Guild Wars?
 // If set to yes, monster transforming is automatically removed/disabled when enterting castles during WoE times
 mon_trans_disable_in_gvg: no

+ 15 - 0
conf/char_athena.conf

@@ -261,4 +261,19 @@ default_map_y: 191
 // Default: 14
 clan_remove_inactive_days: 14
 
+//===================================
+// RODEX
+//===================================
+// After how many days should mails be returned to their sender?
+// 0: never return them
+// X: return them after X days
+// Default: 14
+mail_return_days: 14
+
+// How many days after a mail was returned to it's sender should it be deleted completely?
+// 0: never delete them
+// X: delete them X days after they were returned
+// Default: 14
+mail_delete_days: 14
+
 import: conf/import/char_conf.txt

+ 1 - 0
conf/inter_athena.conf

@@ -108,6 +108,7 @@ party_db: party
 pet_db: pet
 friend_db: friends
 mail_db: mail
+mail_attachment_db: mail_attachments
 auction_db: auction
 quest_db: quest
 homunculus_db: homunculus

+ 23 - 14
db/packet_db.txt

@@ -2497,31 +2497,31 @@ packet_keys: 0x62C86D09,0x75944F17,0x112C133D // [YomRawr]
 
 // RODEX Mail system
 0x09E7,3		// ZC_NOTIFY_UNREADMAIL
-0x09E8,11,dull,0	// CZ_OPEN_MAILBOX
+0x09E8,11,mailrefresh,2:3	// CZ_OPEN_MAILBOX
 0x09E9,2,dull,0 	// CZ_CLOSE_MAILBOX
-0x09EA,11,dull,0	// CZ_REQ_READ_MAIL
+0x09EA,11,mailread,2:3	// CZ_REQ_READ_MAIL
 0x09EB,-1		// ZC_ACK_READ_MAIL
-0x09EC,-1,dull,0	// CZ_REQ_WRITE_MAIL
+0x09EC,-1,mailsend,2:4:28:52:60:62:64	// CZ_REQ_WRITE_MAIL
 0x09ED,3		// ZC_ACK_WRITE_MAIL
-0x09EE,11,dull,0	// CZ_REQ_NEXT_MAIL_LIST
-0x09EF,11,dull,0 	// CZ_REQ_REFRESH_MAIL_LIST
+0x09EE,11,mailrefresh,2:3	// CZ_REQ_NEXT_MAIL_LIST
+0x09EF,11,mailrefresh,2:3 	// CZ_REQ_REFRESH_MAIL_LIST
 0x09F0,-1		// ZC_ACK_MAIL_LIST
-0x09F1,11,dull,0	// CZ_REQ_ZENY_FROM_MAIL
+0x09F1,11,mailgetattach,0	// CZ_REQ_ZENY_FROM_MAIL
 0x09F2,12	// ZC_ACK_ZENY_FROM_MAIL
-0x09F3,11,dull,0	// CZ_REQ_ITEM_FROM_MAIL
+0x09F3,11,mailgetattach,0	// CZ_REQ_ITEM_FROM_MAIL
 0x09F4,12	// ZC_ACK_ITEM_FROM_MAIL
-0x09F5,11,dull,0	// CZ_REQ_DELETE_MAIL
+0x09F5,11,maildelete,0	// CZ_REQ_DELETE_MAIL
 0x09F6,11		// ZC_ACK_DELETE_MAIL
-0x0A03,2,dull,0	// CZ_REQ_CANCEL_WRITE_MAIL
-0x0A04,6,dull,0	// CZ_REQ_ADD_ITEM_TO_MAIL
+0x0A03,2,mailcancel,0	// CZ_REQ_CANCEL_WRITE_MAIL
+0x0A04,6,mailsetattach,2:4	// CZ_REQ_ADD_ITEM_TO_MAIL
 0x0A05,53	// ZC_ACK_ADD_ITEM_TO_MAIL
-0x0A06,6,dull,0	// CZ_REQ_REMOVE_ITEM_MAIL
+0x0A06,6,mailwinopen,2:4	// CZ_REQ_REMOVE_ITEM_MAIL
 0x0A07,9		// ZC_ACK_REMOVE_ITEM_MAIL
-0x0A08,26,dull,0	// CZ_REQ_OPEN_WRITE_MAIL
+0x0A08,26,mailbegin,0	// CZ_REQ_OPEN_WRITE_MAIL
 0x0A12,27	// ZC_ACK_OPEN_WRITE_MAIL
-0x0A32,2		// ZC_OPEN_RODEX_THROUGH_NPC_ONLY
-0x0A13,26,dull,0	// CZ_CHECK_RECEIVE_CHARACTER_NAME
+0x0A13,26,mailreceiver,2	// CZ_CHECK_RECEIVE_CHARACTER_NAME
 0x0A14,10		// ZC_CHECK_RECEIVE_CHARACTER_NAME
+0x0A32,2		// ZC_OPEN_RODEX_THROUGH_NPC_ONLY
 
 // New EquipPackets Support
 0x0A09,45	// ZC_ADD_EXCHANGE_ITEM3
@@ -2657,6 +2657,15 @@ packet_keys: 0x4C17382A,0x7ED174C9,0x29961E4F // [Winnie]
 0x088D,5,hommenu,2:4
 0x0940,36,storagepassword,2:4:20
 
+// 2016-03-02bRagexe
+0x0A51,34
+
+// 2016-03-30aRagexe
+0x0A6E,-1,mailsend,2:4:28:52:60:62:64:68	// CZ_REQ_WRITE_MAIL2
+
+// 2016-06-01aRagexe
+0x0A7D,-1
+
 // 2017-05-24aRagexeRE
 0x0A43,85
 0x0A44,-1

+ 39 - 9
doc/packet_interserv.txt

@@ -1006,13 +1006,14 @@ Currently the max packet size is 0xFFFF (see 'WFIFOSET()' in 'src/common/socket.
 
 0x3048
 	Type: ZI
-	Structure: <cmd>.W <cid>.L <flag>.B
-	index: 0,2,6
-	len: 7
+	Structure: <cmd>.W <cid>.L <flag>.B <mail_type>.B
+	index: 0,2,6,7
+	len: 8
 	parameter:
 		- cmd : packet identification (0x3048)
 		- cid
 		- flag
+		- mail_type
 	desc:
 		- Inbox request
 
@@ -1029,13 +1030,14 @@ Currently the max packet size is 0xFFFF (see 'WFIFOSET()' in 'src/common/socket.
 
 0x304a
 	Type: ZI
-	Structure: <cmd>.W <cid>.L <mail_id>.L
-	index: 0,2,6
-	len: 10
+	Structure: <cmd>.W <cid>.L <mail_id>.L <attachment_type>.B
+	index: 0,2,6,10
+	len: 11
 	parameter:
 		- cmd : packet identification (0x304a)
 		- cid
 		- mail_id
+		- attachment_type
 	desc:
 		- Mail get attachment
 
@@ -1076,6 +1078,18 @@ Currently the max packet size is 0xFFFF (see 'WFIFOSET()' in 'src/common/socket.
 	desc:
 		- Mail send
 
+0x304e
+	Type: ZI
+	Structure: <cmd>.W <cid>.L <name>.24B
+	index: 0,2,6
+	len: 30
+	parameter:
+		- cmd : packet identification (0x304e)
+		- cid
+		- name
+	desc:
+		- Checks if a character with the given name exists.
+
 0x3050
 	Type: ZI
 	Structure: <cmd>.W <len>.W <cid>.L <type>.W <price>.L <page>.W <searchtext>.?B
@@ -1966,14 +1980,15 @@ Currently the max packet size is 0xFFFF (see 'WFIFOSET()' in 'src/common/socket.
 
 0x3848
 	Type: IZ
-	Structure: <cmd>.W <size>.W <char_id>.L <flag>.B <md>.?B
-	index: 0,2,4,8,9
-	len: variable: 9+md
+	Structure: <cmd>.W <size>.W <char_id>.L <flag>.B <mail_type>.B <md>.?B
+	index: 0,2,4,8,9,10
+	len: variable: 10+md
 	parameter:
 		- cmd : packet identification (0x3848)
 		- size
 		- char_id
 		- flag
+		- mail_type
 		- md : Mail
 	desc:
 		- A player request for mail inbox
@@ -2044,6 +2059,21 @@ Currently the max packet size is 0xFFFF (see 'WFIFOSET()' in 'src/common/socket.
 	desc:
 		- Mail sent status (to player if the sender is player and online)
 
+0x384e
+	Type: IZ
+	Structure: <cmd>.W <cid_sender>.L <cid_receiver>.L <class>.W <level>.W <name>.24B
+	index: 0,2,6,10,12,14
+	len: 38
+	parameter:
+		- cmd : packet identification (0x384e)
+		- cid_sender
+		- cid_receiver
+		- class
+		- level
+		- name
+	desc:
+		- Mail receiver's character data(character id, job, level and name)
+
 0x3850
 	Type: IZ
 	Structure: <cmd>.W <size>.W <char_id>.L <count>.W <pages>.W <auction_data>.?B

+ 6 - 0
doc/script_commands.txt

@@ -6097,6 +6097,12 @@ while hidden then revealing.... you can wonder around =P
 
 ---------------------------------------
 
+*unloadnpc "<NPC object name>";
+
+This command will fully unload a NPC object and all of it's duplicates.
+
+---------------------------------------
+
 *doevent "<NPC object name>::<event label>";
 
 This command will start a new execution thread in a specified NPC object at the 

+ 11 - 0
npc/other/mail.txt

@@ -137,3 +137,14 @@ rachel,122,146,0	duplicate(MailBox)	Post Box#ra	888
 // Veins
 //============================================================
 veins,218,123,0	duplicate(MailBox)	Post Box#ve	888
+
+// RODEX makes these NPCs useless
+-	script	RodexMailBoxInit	-1,{
+	end;
+
+OnInit:
+	if( PACKETVER >= 20150513 ){
+		unloadnpc "MailBox";
+	}
+	end;
+}

+ 37 - 26
sql-files/main.sql

@@ -716,35 +716,46 @@ CREATE TABLE IF NOT EXISTS `mail` (
   `time` int(11) unsigned NOT NULL default '0',
   `status` tinyint(2) NOT NULL default '0',
   `zeny` int(11) unsigned NOT NULL default '0',
-  `nameid` smallint(5) unsigned NOT NULL default '0',
-  `amount` int(11) unsigned NOT NULL default '0',
-  `refine` tinyint(3) unsigned NOT NULL default '0',
-  `attribute` tinyint(4) unsigned NOT NULL default '0',
-  `identify` smallint(6) NOT NULL default '0',
-  `card0` smallint(5) unsigned NOT NULL default '0',
-  `card1` smallint(5) unsigned NOT NULL default '0',
-  `card2` smallint(5) unsigned NOT NULL default '0',
-  `card3` smallint(5) unsigned NOT NULL default '0',
-  `option_id0` smallint(5) unsigned NOT NULL default '0',
-  `option_val0` smallint(5) unsigned NOT NULL default '0',
-  `option_parm0` tinyint(3) unsigned NOT NULL default '0',
-  `option_id1` smallint(5) unsigned NOT NULL default '0',
-  `option_val1` smallint(5) unsigned NOT NULL default '0',
-  `option_parm1` tinyint(3) unsigned NOT NULL default '0',
-  `option_id2` smallint(5) unsigned NOT NULL default '0',
-  `option_val2` smallint(5) unsigned NOT NULL default '0',
-  `option_parm2` tinyint(3) unsigned NOT NULL default '0',
-  `option_id3` smallint(5) unsigned NOT NULL default '0',
-  `option_val3` smallint(5) unsigned NOT NULL default '0',
-  `option_parm3` tinyint(3) unsigned NOT NULL default '0',
-  `option_id4` smallint(5) unsigned NOT NULL default '0',
-  `option_val4` smallint(5) unsigned NOT NULL default '0',
-  `option_parm4` tinyint(3) unsigned NOT NULL default '0',
-  `unique_id` bigint(20) unsigned NOT NULL default '0',
-  `bound` tinyint(1) unsigned NOT NULL default '0',
+  `type` smallint(5) NOT NULL default '0',
   PRIMARY KEY  (`id`)
 ) ENGINE=MyISAM;
 
+-- ----------------------------
+-- Table structure for `mail_attachments`
+-- ----------------------------
+
+CREATE TABLE IF NOT EXISTS `mail_attachments` (
+  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+  `index` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `nameid` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `amount` int(11) unsigned NOT NULL DEFAULT '0',
+  `refine` tinyint(3) unsigned NOT NULL DEFAULT '0',
+  `attribute` tinyint(4) unsigned NOT NULL DEFAULT '0',
+  `identify` smallint(6) NOT NULL DEFAULT '0',
+  `card0` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `card1` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `card2` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `card3` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `option_id0` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `option_val0` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `option_parm0` tinyint(3) unsigned NOT NULL DEFAULT '0',
+  `option_id1` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `option_val1` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `option_parm1` tinyint(3) unsigned NOT NULL DEFAULT '0',
+  `option_id2` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `option_val2` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `option_parm2` tinyint(3) unsigned NOT NULL DEFAULT '0',
+  `option_id3` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `option_val3` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `option_parm3` tinyint(3) unsigned NOT NULL DEFAULT '0',
+  `option_id4` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `option_val4` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `option_parm4` tinyint(3) unsigned NOT NULL DEFAULT '0',
+  `unique_id` bigint(20) unsigned NOT NULL DEFAULT '0',
+  `bound` tinyint(1) unsigned NOT NULL DEFAULT '0',
+    PRIMARY KEY (`id`,`index`)
+) ENGINE=MyISAM;
+
 --
 -- Table structure for table `mapreg`
 --

+ 76 - 0
sql-files/upgrades/upgrade_20170620.sql

@@ -0,0 +1,76 @@
+-- ----------------------------
+-- Table structure for `mail_attachments`
+-- ----------------------------
+
+CREATE TABLE IF NOT EXISTS `mail_attachments` (
+  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+  `index` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `nameid` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `amount` int(11) unsigned NOT NULL DEFAULT '0',
+  `refine` tinyint(3) unsigned NOT NULL DEFAULT '0',
+  `attribute` tinyint(4) unsigned NOT NULL DEFAULT '0',
+  `identify` smallint(6) NOT NULL DEFAULT '0',
+  `card0` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `card1` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `card2` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `card3` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `option_id0` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `option_val0` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `option_parm0` tinyint(3) unsigned NOT NULL DEFAULT '0',
+  `option_id1` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `option_val1` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `option_parm1` tinyint(3) unsigned NOT NULL DEFAULT '0',
+  `option_id2` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `option_val2` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `option_parm2` tinyint(3) unsigned NOT NULL DEFAULT '0',
+  `option_id3` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `option_val3` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `option_parm3` tinyint(3) unsigned NOT NULL DEFAULT '0',
+  `option_id4` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `option_val4` smallint(5) unsigned NOT NULL DEFAULT '0',
+  `option_parm4` tinyint(3) unsigned NOT NULL DEFAULT '0',
+  `unique_id` bigint(20) unsigned NOT NULL DEFAULT '0',
+  `bound` tinyint(1) unsigned NOT NULL DEFAULT '0',
+    PRIMARY KEY (`id`,`index`)
+) ENGINE=MyISAM;
+
+insert into `mail_attachments`
+(`id`,`index`,`nameid`,`amount`,`refine`,`attribute`,`identify`,`card0`,`card1`,`card2`,`card3`,`option_id0`,`option_val0`,`option_parm0`,`option_id1`,`option_val1`,`option_parm1`,`option_id2`,`option_val2`,`option_parm2`,`option_id3`,`option_val3`,`option_parm3`,`option_id4`,`option_val4`,`option_parm4`,`unique_id`,`bound`)
+select
+`id`,'0',`nameid`,`amount`,`refine`,`attribute`,`identify`,`card0`,`card1`,`card2`,`card3`,`option_id0`,`option_val0`,`option_parm0`,`option_id1`,`option_val1`,`option_parm1`,`option_id2`,`option_val2`,`option_parm2`,`option_id3`,`option_val3`,`option_parm3`,`option_id4`,`option_val4`,`option_parm4`,`unique_id`,`bound`
+from `mail`
+where `nameid` <> 0;
+
+alter table `mail`
+	drop column `nameid`,
+	drop column `amount`,
+	drop column `refine`,
+	drop column `attribute`,
+	drop column `identify`,
+	drop column `card0`,
+	drop column `card1`,
+	drop column `card2`,
+	drop column `card3`,
+	drop column `option_id0`,
+	drop column `option_val0`,
+	drop column `option_parm0`,
+	drop column `option_id1`,
+	drop column `option_val1`,
+	drop column `option_parm1`,
+	drop column `option_id2`,
+	drop column `option_val2`,
+	drop column `option_parm2`,
+	drop column `option_id3`,
+	drop column `option_val3`,
+	drop column `option_parm3`,
+	drop column `option_id4`,
+	drop column `option_val4`,
+	drop column `option_parm4`,
+	drop column `unique_id`,
+	drop column `bound`;
+
+alter table `mail`
+	modify `message` varchar(500) NOT NULL default '';
+
+ALTER TABLE `mail`
+	ADD COLUMN `type` smallint(5) NOT NULL default '0';

+ 31 - 3
src/char/char.c

@@ -20,6 +20,7 @@
 #include "../common/cli.h"
 #include "int_guild.h"
 #include "int_homun.h"
+#include "int_mail.h"
 #include "int_mercenary.h"
 #include "int_elemental.h"
 #include "int_party.h"
@@ -1649,6 +1650,10 @@ int char_delete_char_sql(uint32 char_id){
 	if( SQL_ERROR == Sql_Query(sql_handle, "DELETE FROM `%s` WHERE `char_id`='%d'", schema_config.skill_db, char_id) )
 		Sql_ShowDebug(sql_handle);
 
+	/* delete mail attachments (only received) */
+	if (SQL_ERROR == Sql_Query(sql_handle, "DELETE FROM `%s` WHERE `id` IN ( SELECT `id` FROM `%s` WHERE `dest_id`='%d' )", schema_config.mail_attachment_db, schema_config.mail_db, char_id))
+		Sql_ShowDebug(sql_handle);
+	
 	/* delete mails (only received) */
 	if (SQL_ERROR == Sql_Query(sql_handle, "DELETE FROM `%s` WHERE `dest_id`='%d'", schema_config.mail_db, char_id))
 		Sql_ShowDebug(sql_handle);
@@ -2228,7 +2233,7 @@ bool char_checkdb(void){
                 schema_config.auction_db, schema_config.quest_db, schema_config.homunculus_db, schema_config.skill_homunculus_db,
                 schema_config.mercenary_db, schema_config.mercenary_owner_db,
 		schema_config.elemental_db, schema_config.ragsrvinfo_db, schema_config.skillcooldown_db, schema_config.bonus_script_db,
-		schema_config.clan_table, schema_config.clan_alliance_table
+		schema_config.clan_table, schema_config.clan_alliance_table, schema_config.mail_attachment_db
 	};
 	ShowInfo("Start checking DB integrity\n");
 	for (i=0; i<ARRAYLENGTH(sqltable); i++){ //check if they all exist and we can acces them in sql-server
@@ -2365,12 +2370,19 @@ bool char_checkdb(void){
 	}
 	//checking mail_db
 	if( SQL_ERROR == Sql_Query(sql_handle, "SELECT  `id`,`send_name`,`send_id`,`dest_name`,`dest_id`,"
-			"`title`,`message`,`time`,`status`,`zeny`,`nameid`,`amount`,`refine`,`attribute`,`identify`,"
-			"`card0`,`card1`,`card2`,`card3`,`option_id0`,`option_val0`,`option_parm0`,`option_id1`,`option_val1`,`option_parm1`,`option_id2`,`option_val2`,`option_parm2`,`option_id3`,`option_val3`,`option_parm3`,`option_id4`,`option_val4`,`option_parm4`,`unique_id`, `bound`"
+			"`title`,`message`,`time`,`status`,`zeny`,`type`"
 			" FROM `%s` LIMIT 1;", schema_config.mail_db) ){
 		Sql_ShowDebug(sql_handle);
 		return false;
 	}
+	//checking mail_attachment_db
+	if( SQL_ERROR == Sql_Query(sql_handle, "SELECT  `id`,`index`,`nameid`,`amount`,`refine`,`attribute`,`identify`,"
+			"`card0`,`card1`,`card2`,`card3`,`option_id0`,`option_val0`,`option_parm0`,`option_id1`,`option_val1`,`option_parm1`,`option_id2`,`option_val2`,`option_parm2`,`option_id3`,`option_val3`,`option_parm3`,`option_id4`,`option_val4`,`option_parm4`,`unique_id`, `bound`"
+			" FROM `%s` LIMIT 1;", schema_config.mail_attachment_db) ){
+		Sql_ShowDebug(sql_handle);
+		return false;
+	}
+
 	//checking auction_db
 	if( SQL_ERROR == Sql_Query(sql_handle, "SELECT  `auction_id`,`seller_id`,`seller_name`,`buyer_id`,`buyer_name`,"
 			"`price`,`buynow`,`hours`,`timestamp`,`nameid`,`item_name`,`type`,`refine`,`attribute`,`card0`,`card1`,"
@@ -2522,6 +2534,8 @@ void char_sql_config_read(const char* cfgName) {
 			safestrncpy(schema_config.pet_db, w2, sizeof(schema_config.pet_db));
 		else if(!strcmpi(w1,"mail_db"))
 			safestrncpy(schema_config.mail_db, w2, sizeof(schema_config.mail_db));
+		else if (!strcmpi(w1, "mail_attachment_db"))
+			safestrncpy(schema_config.mail_attachment_db, w2, sizeof(schema_config.mail_attachment_db));
 		else if(!strcmpi(w1,"auction_db"))
 			safestrncpy(schema_config.auction_db, w2, sizeof(schema_config.auction_db));
 		else if(!strcmpi(w1,"friend_db"))
@@ -2699,6 +2713,8 @@ void char_set_defaults(){
 	charserv_config.default_map_y = 191;
 
 	charserv_config.clan_remove_inactive_days = 14;
+	charserv_config.mail_return_days = 14;
+	charserv_config.mail_delete_days = 14;
 }
 
 /**
@@ -2973,6 +2989,10 @@ bool char_config_read(const char* cfgName, bool normal){
 			charserv_config.default_map_y = atoi(w2);
 		} else if (strcmpi(w1, "clan_remove_inactive_days") == 0) {
 			charserv_config.clan_remove_inactive_days = atoi(w2);
+		} else if (strcmpi(w1, "mail_return_days") == 0) {
+			charserv_config.mail_return_days = atoi(w2);
+		} else if (strcmpi(w1, "mail_delete_days") == 0) {
+			charserv_config.mail_delete_days = atoi(w2);
 		} else if (strcmpi(w1, "import") == 0) {
 			char_config_read(w2, normal);
 		}
@@ -3147,6 +3167,14 @@ int do_init(int argc, char **argv)
 	add_timer_func_list(char_clan_member_cleanup, "clan_member_cleanup");
 	add_timer_interval(gettick() + 1000, char_clan_member_cleanup, 0, 0, 60 * 60 * 1000); // every 60 minutes
 
+	// periodically check if mails need to be returned to their sender
+	add_timer_func_list(mail_return_timer, "mail_return_timer");
+	add_timer_interval(gettick() + 1000, mail_return_timer, 0, 0, 5 * 60 * 1000); // every 5 minutes
+
+	// periodically check if mails need to be deleted completely
+	add_timer_func_list(mail_delete_timer, "mail_delete_timer");
+	add_timer_interval(gettick() + 1000, mail_delete_timer, 0, 0, 5 * 60 * 1000); // every 5 minutes
+
 	//check db tables
 	if(charserv_config.char_check_db && char_checkdb() == 0){
 		ShowFatalError("char : A tables is missing in sql-server, please fix it, see (sql-files main.sql for structure) \n");

+ 3 - 0
src/char/char.h

@@ -58,6 +58,7 @@ struct Schema_Config {
 	char party_db[DB_NAME_LEN];
 	char pet_db[DB_NAME_LEN];
 	char mail_db[DB_NAME_LEN]; // MAIL SYSTEM
+	char mail_attachment_db[DB_NAME_LEN];
 	char auction_db[DB_NAME_LEN]; // Auctions System
 	char friend_db[DB_NAME_LEN];
 	char hotkey_db[DB_NAME_LEN];
@@ -167,6 +168,8 @@ struct CharServ_Config {
 	unsigned short default_map_y;
 
 	int clan_remove_inactive_days;
+	int mail_return_days;
+	int mail_delete_days;
 };
 extern struct CharServ_Config charserv_config;
 

+ 10 - 10
src/char/int_auction.c

@@ -154,12 +154,12 @@ static int auction_end_timer(int tid, unsigned int tick, int id, intptr_t data)
 	{
 		if( auction->buyer_id )
 		{
-			mail_sendmail(0, msg_txt(200), auction->buyer_id, auction->buyer_name, msg_txt(201), msg_txt(202), 0, &auction->item);
+			mail_sendmail(0, msg_txt(200), auction->buyer_id, auction->buyer_name, msg_txt(201), msg_txt(202), 0, &auction->item, 1);
 			mapif_Auction_message(auction->buyer_id, 6); // You have won the auction
-			mail_sendmail(0, msg_txt(200), auction->seller_id, auction->seller_name, msg_txt(201), msg_txt(203), auction->price, NULL);
+			mail_sendmail(0, msg_txt(200), auction->seller_id, auction->seller_name, msg_txt(201), msg_txt(203), auction->price, NULL, 0);
 		}
 		else
-			mail_sendmail(0, msg_txt(200), auction->seller_id, auction->seller_name, msg_txt(201), msg_txt(204), 0, &auction->item);
+			mail_sendmail(0, msg_txt(200), auction->seller_id, auction->seller_name, msg_txt(201), msg_txt(204), 0, &auction->item, 1);
 
 		ShowInfo("Auction End: id %u.\n", auction->auction_id);
 
@@ -376,7 +376,7 @@ static void mapif_parse_Auction_cancel(int fd)
 		return;
 	}
 
-	mail_sendmail(0, msg_txt(200), auction->seller_id, auction->seller_name, msg_txt(201), msg_txt(205), 0, &auction->item);
+	mail_sendmail(0, msg_txt(200), auction->seller_id, auction->seller_name, msg_txt(201), msg_txt(205), 0, &auction->item, 1);
 	auction_delete(auction);
 
 	mapif_Auction_cancel(fd, char_id, 0); // The auction has been canceled
@@ -415,9 +415,9 @@ static void mapif_parse_Auction_close(int fd)
 	}
 
 	// Send Money to Seller
-	mail_sendmail(0, msg_txt(200), auction->seller_id, auction->seller_name, msg_txt(201), msg_txt(206), auction->price, NULL);
+	mail_sendmail(0, msg_txt(200), auction->seller_id, auction->seller_name, msg_txt(201), msg_txt(206), auction->price, NULL, 0);
 	// Send Item to Buyer
-	mail_sendmail(0, msg_txt(200), auction->buyer_id, auction->buyer_name, msg_txt(201), msg_txt(207), 0, &auction->item);
+	mail_sendmail(0, msg_txt(200), auction->buyer_id, auction->buyer_name, msg_txt(201), msg_txt(207), 0, &auction->item, 1);
 	mapif_Auction_message(auction->buyer_id, 6); // You have won the auction
 	auction_delete(auction);
 
@@ -456,11 +456,11 @@ static void mapif_parse_Auction_bid(int fd)
 	{ // Send Money back to the previous Buyer
 		if( auction->buyer_id != char_id )
 		{
-			mail_sendmail(0, msg_txt(200), auction->buyer_id, auction->buyer_name, msg_txt(201), msg_txt(208), auction->price, NULL);
+			mail_sendmail(0, msg_txt(200), auction->buyer_id, auction->buyer_name, msg_txt(201), msg_txt(208), auction->price, NULL, 0);
 			mapif_Auction_message(auction->buyer_id, 7); // You have failed to win the auction
 		}
 		else
-			mail_sendmail(0, msg_txt(200), auction->buyer_id, auction->buyer_name, msg_txt(201), msg_txt(209), auction->price, NULL);
+			mail_sendmail(0, msg_txt(200), auction->buyer_id, auction->buyer_name, msg_txt(201), msg_txt(209), auction->price, NULL, 0);
 	}
 
 	auction->buyer_id = char_id;
@@ -471,9 +471,9 @@ static void mapif_parse_Auction_bid(int fd)
 	{ // Automatic won the auction
 		mapif_Auction_bid(fd, char_id, bid - auction->buynow, 1); // You have successfully bid in the auction
 
-		mail_sendmail(0, msg_txt(200), auction->buyer_id, auction->buyer_name, msg_txt(201), msg_txt(210), 0, &auction->item);
+		mail_sendmail(0, msg_txt(200), auction->buyer_id, auction->buyer_name, msg_txt(201), msg_txt(210), 0, &auction->item, 1);
 		mapif_Auction_message(char_id, 6); // You have won the auction
-		mail_sendmail(0, msg_txt(200), auction->seller_id, auction->seller_name, msg_txt(201), msg_txt(211), auction->buynow, NULL);
+		mail_sendmail(0, msg_txt(200), auction->seller_id, auction->seller_name, msg_txt(201), msg_txt(211), auction->buynow, NULL, 0);
 
 		auction_delete(auction);
 		return;

+ 320 - 169
src/char/int_mail.c

@@ -12,87 +12,47 @@
 
 #include <stdlib.h>
 
+static bool mail_loadmessage(int mail_id, struct mail_message* msg);
+static void mapif_Mail_return(int fd, uint32 char_id, int mail_id);
+static void mapif_Mail_delete(int fd, uint32 char_id, int mail_id, bool deleted);
+
 static int mail_fromsql(uint32 char_id, struct mail_data* md)
 {
-	int i, j;
-	struct mail_message *msg;
+	int i;
 	char *data;
-	StringBuf buf;
 
 	memset(md, 0, sizeof(struct mail_data));
 	md->amount = 0;
 	md->full = false;
 
-	StringBuf_Init(&buf);
-	StringBuf_AppendStr(&buf, "SELECT `id`,`send_name`,`send_id`,`dest_name`,`dest_id`,`title`,`message`,`time`,`status`,"
-		"`zeny`,`amount`,`nameid`,`refine`,`attribute`,`identify`,`unique_id`,`bound`");
-	for (i = 0; i < MAX_SLOTS; i++)
-		StringBuf_Printf(&buf, ",`card%d`", i);
-	for (i = 0; i < MAX_ITEM_RDM_OPT; ++i) {
-		StringBuf_Printf(&buf, ", `option_id%d`", i);
-		StringBuf_Printf(&buf, ", `option_val%d`", i);
-		StringBuf_Printf(&buf, ", `option_parm%d`", i);
-	}
-	// I keep the `status` < 3 just in case someone forget to apply the sqlfix
-	StringBuf_Printf(&buf, " FROM `%s` WHERE `dest_id`='%d' AND `status` < 3 ORDER BY `id` LIMIT %d",
-		schema_config.mail_db, char_id, MAIL_MAX_INBOX + 1);
-
-	if( SQL_ERROR == Sql_Query(sql_handle, StringBuf_Value(&buf)) )
+	// First we prefill the msg ids
+	if( SQL_ERROR == Sql_Query(sql_handle, "SELECT `id` FROM `%s` WHERE `dest_id`='%d' AND `status` < 3 ORDER BY `id` LIMIT %d", schema_config.mail_db, char_id, MAIL_MAX_INBOX + 1) ){
 		Sql_ShowDebug(sql_handle);
+		return 0;
+	}
 
-	StringBuf_Destroy(&buf);
-
-	for (i = 0; i < MAIL_MAX_INBOX && SQL_SUCCESS == Sql_NextRow(sql_handle); ++i )
-	{
-		struct item *item;
-
-		msg = &md->msg[i];
-		Sql_GetData(sql_handle, 0, &data, NULL); msg->id = atoi(data);
-		Sql_GetData(sql_handle, 1, &data, NULL); safestrncpy(msg->send_name, data, NAME_LENGTH);
-		Sql_GetData(sql_handle, 2, &data, NULL); msg->send_id = strtoul(data, NULL, 10);
-		Sql_GetData(sql_handle, 3, &data, NULL); safestrncpy(msg->dest_name, data, NAME_LENGTH);
-		Sql_GetData(sql_handle, 4, &data, NULL); msg->dest_id = strtoul(data, NULL, 10);
-		Sql_GetData(sql_handle, 5, &data, NULL); safestrncpy(msg->title, data, MAIL_TITLE_LENGTH);
-		Sql_GetData(sql_handle, 6, &data, NULL); safestrncpy(msg->body, data, MAIL_BODY_LENGTH);
-		Sql_GetData(sql_handle, 7, &data, NULL); msg->timestamp = atoi(data); //strtoull ?
-		Sql_GetData(sql_handle, 8, &data, NULL); msg->status = (mail_status)atoi(data);
-		Sql_GetData(sql_handle, 9, &data, NULL); msg->zeny = strtoul(data, NULL, 10);
-		item = &msg->item;
-		Sql_GetData(sql_handle,10, &data, NULL); item->amount = (short)atoi(data);
-		Sql_GetData(sql_handle,11, &data, NULL); item->nameid = atoi(data);
-		Sql_GetData(sql_handle,12, &data, NULL); item->refine = atoi(data);
-		Sql_GetData(sql_handle,13, &data, NULL); item->attribute = atoi(data);
-		Sql_GetData(sql_handle,14, &data, NULL); item->identify = atoi(data);
-		Sql_GetData(sql_handle,15, &data, NULL); item->unique_id = strtoull(data, NULL, 10);
-		Sql_GetData(sql_handle,16, &data, NULL); item->bound = atoi(data);
-		item->expire_time = 0;
-
-		for (j = 0; j < MAX_SLOTS; j++)
-		{
-			Sql_GetData(sql_handle, 17 + j, &data, NULL);
-			item->card[j] = atoi(data);
-		}
+	md->full = (Sql_NumRows(sql_handle) > MAIL_MAX_INBOX);
 
-		for (j = 0; j < MAX_ITEM_RDM_OPT; j++) {
-			Sql_GetData(sql_handle, 17 + MAX_SLOTS + j * 3, &data, NULL);
-			item->option[j].id = atoi(data);
-			Sql_GetData(sql_handle, 18 + MAX_SLOTS + j * 3, &data, NULL);
-			item->option[j].value = atoi(data);
-			Sql_GetData(sql_handle, 19 + MAX_SLOTS + j * 3, &data, NULL);
-			item->option[j].param = atoi(data);
-		}
+	for( i = 0; i < MAIL_MAX_INBOX && SQL_SUCCESS == Sql_NextRow(sql_handle); i++ ){
+		Sql_GetData(sql_handle, 0, &data, NULL); md->msg[i].id = atoi(data);
 	}
 
-	md->full = ( Sql_NumRows(sql_handle) > MAIL_MAX_INBOX );
-
 	md->amount = i;
 	Sql_FreeResult(sql_handle);
 
+	// Now we load them
+	for( i = 0; i < md->amount; i++ ){
+		if( !mail_loadmessage( md->msg[i].id, &md->msg[i] ) ){
+			return 0;
+		}
+	}
+
 	md->unchecked = 0;
 	md->unread = 0;
 	for (i = 0; i < md->amount; i++)
 	{
-		msg = &md->msg[i];
+		struct mail_message *msg = &md->msg[i];
+
 		if( msg->status == MAIL_NEW )
 		{
 			if ( SQL_ERROR == Sql_Query(sql_handle, "UPDATE `%s` SET `status` = '%d' WHERE `id` = '%d'", schema_config.mail_db, MAIL_UNREAD, msg->id) )
@@ -115,27 +75,13 @@ int mail_savemessage(struct mail_message* msg)
 {
 	StringBuf buf;
 	SqlStmt* stmt;
-	int j;
+	int i, j;
+	bool found = false;
 
 	// build message save query
 	StringBuf_Init(&buf);
-	StringBuf_Printf(&buf, "INSERT INTO `%s` (`send_name`, `send_id`, `dest_name`, `dest_id`, `title`, `message`, `time`, `status`, `zeny`, `amount`, `nameid`, `refine`, `attribute`, `identify`, `unique_id`, `bound`", schema_config.mail_db);
-	for (j = 0; j < MAX_SLOTS; j++)
-		StringBuf_Printf(&buf, ", `card%d`", j);
-	for (j = 0; j < MAX_ITEM_RDM_OPT; ++j) {
-		StringBuf_Printf(&buf, ", `option_id%d`", j);
-		StringBuf_Printf(&buf, ", `option_val%d`", j);
-		StringBuf_Printf(&buf, ", `option_parm%d`", j);
-	}
-	StringBuf_Printf(&buf, ") VALUES (?, '%d', ?, '%d', ?, ?, '%lu', '%d', '%d', '%d', '%hu', '%d', '%d', '%d', '%"PRIu64"', '%d'",
-		msg->send_id, msg->dest_id, (unsigned long)msg->timestamp, msg->status, msg->zeny, msg->item.amount, msg->item.nameid, msg->item.refine, msg->item.attribute, msg->item.identify, msg->item.unique_id, msg->item.bound);
-	for (j = 0; j < MAX_SLOTS; j++)
-		StringBuf_Printf(&buf, ", '%hu'", msg->item.card[j]);
-	for (j = 0; j < MAX_ITEM_RDM_OPT; ++j) {
-		StringBuf_Printf(&buf, ", '%d'", msg->item.option[j].id);
-		StringBuf_Printf(&buf, ", '%d'", msg->item.option[j].value);
-		StringBuf_Printf(&buf, ", '%d'", msg->item.option[j].param);
-	}
+	StringBuf_Printf(&buf, "INSERT INTO `%s` (`send_name`, `send_id`, `dest_name`, `dest_id`, `title`, `message`, `time`, `status`, `zeny`,`type`", schema_config.mail_db);
+	StringBuf_Printf(&buf, ") VALUES (?, '%d', ?, '%d', ?, ?, '%lu', '%d', '%d', '%d'", msg->send_id, msg->dest_id, (unsigned long)msg->timestamp, msg->status, msg->zeny, msg->type);
 	StringBuf_AppendStr(&buf, ")");
 
 	// prepare and execute query
@@ -148,11 +94,50 @@ int mail_savemessage(struct mail_message* msg)
 	||  SQL_SUCCESS != SqlStmt_Execute(stmt) )
 	{
 		SqlStmt_ShowDebug(stmt);
-		msg->id = 0;
+		StringBuf_Destroy(&buf);
+		return msg->id = 0;
 	} else
 		msg->id = (int)SqlStmt_LastInsertId(stmt);
 
 	SqlStmt_Free(stmt);
+	
+	StringBuf_Clear(&buf);
+	StringBuf_Printf(&buf,"INSERT INTO `%s` (`id`, `index`, `amount`, `nameid`, `refine`, `attribute`, `identify`, `unique_id`, `bound`", schema_config.mail_attachment_db);
+	for (j = 0; j < MAX_SLOTS; j++)
+		StringBuf_Printf(&buf, ", `card%d`", j);
+	for (j = 0; j < MAX_ITEM_RDM_OPT; ++j) {
+		StringBuf_Printf(&buf, ", `option_id%d`", j);
+		StringBuf_Printf(&buf, ", `option_val%d`", j);
+		StringBuf_Printf(&buf, ", `option_parm%d`", j);
+	}
+	StringBuf_AppendStr(&buf, ") VALUES ");
+
+	for( i = 0; i < MAIL_MAX_ITEM; i++ ){
+		// skip empty and already matched entries
+		if( msg->item[i].nameid == 0 )
+			continue;
+
+		if( found ){
+			StringBuf_AppendStr(&buf, ",");
+		}else{
+			found = true;
+		}
+
+		StringBuf_Printf(&buf, "('%"PRIu64"', '%hu', '%d', '%hu', '%d', '%d', '%d', '%"PRIu64"', '%d'", (uint64)msg->id, i, msg->item[i].amount, msg->item[i].nameid, msg->item[i].refine, msg->item[i].attribute, msg->item[i].identify, msg->item[i].unique_id, msg->item[i].bound);
+		for (j = 0; j < MAX_SLOTS; j++)
+			StringBuf_Printf(&buf, ", '%hu'", msg->item[i].card[j]);
+		for (j = 0; j < MAX_ITEM_RDM_OPT; ++j) {
+			StringBuf_Printf(&buf, ", '%d'", msg->item[i].option[j].id);
+			StringBuf_Printf(&buf, ", '%d'", msg->item[i].option[j].value);
+			StringBuf_Printf(&buf, ", '%d'", msg->item[i].option[j].param);
+		}
+		StringBuf_AppendStr(&buf, ")");
+	}
+
+	if( found && SQL_ERROR == Sql_QueryStr(sql_handle, StringBuf_Value(&buf)) ){
+		Sql_ShowDebug(sql_handle);
+	}
+
 	StringBuf_Destroy(&buf);
 
 	return msg->id;
@@ -162,64 +147,80 @@ int mail_savemessage(struct mail_message* msg)
 /// Returns true if the operation succeeds (or false if it fails).
 static bool mail_loadmessage(int mail_id, struct mail_message* msg)
 {
-	int j;
+	int i, j;
 	StringBuf buf;
+	char* data;
+
+	if( SQL_ERROR == Sql_Query(sql_handle, "SELECT `id`,`send_name`,`send_id`,`dest_name`,`dest_id`,`title`,`message`,`time`,`status`,`zeny`,`type` FROM `%s` WHERE `id` = '%d'", schema_config.mail_db, mail_id )
+	||  SQL_SUCCESS != Sql_NextRow(sql_handle) ){
+		Sql_ShowDebug(sql_handle);
+		Sql_FreeResult(sql_handle);
+		return false;
+	}else{
+		Sql_GetData(sql_handle, 0, &data, NULL); msg->id = atoi(data);
+		Sql_GetData(sql_handle, 1, &data, NULL); safestrncpy(msg->send_name, data, NAME_LENGTH);
+		Sql_GetData(sql_handle, 2, &data, NULL); msg->send_id = atoi(data);
+		Sql_GetData(sql_handle, 3, &data, NULL); safestrncpy(msg->dest_name, data, NAME_LENGTH);
+		Sql_GetData(sql_handle, 4, &data, NULL); msg->dest_id = atoi(data);
+		Sql_GetData(sql_handle, 5, &data, NULL); safestrncpy(msg->title, data, MAIL_TITLE_LENGTH);
+		Sql_GetData(sql_handle, 6, &data, NULL); safestrncpy(msg->body, data, MAIL_BODY_LENGTH);
+		Sql_GetData(sql_handle, 7, &data, NULL); msg->timestamp = atoi(data);
+		Sql_GetData(sql_handle, 8, &data, NULL); msg->status = (mail_status)atoi(data);
+		Sql_GetData(sql_handle, 9, &data, NULL); msg->zeny = atoi(data);
+		Sql_GetData(sql_handle,10, &data, NULL); msg->type = atoi(data);
+
+		if( msg->type == MAIL_INBOX_NORMAL && charserv_config.mail_return_days > 0 ){
+			msg->scheduled_deletion = msg->timestamp + charserv_config.mail_return_days * 24 * 60 * 60;
+		}else if( msg->type == MAIL_INBOX_RETURNED && charserv_config.mail_delete_days > 0 ){
+			msg->scheduled_deletion = msg->timestamp + charserv_config.mail_delete_days * 24 * 60 * 60;
+		}else{
+			msg->scheduled_deletion = 0;
+		}
+
+		Sql_FreeResult(sql_handle);
+	}
 
 	StringBuf_Init(&buf);
-	StringBuf_AppendStr(&buf, "SELECT `id`,`send_name`,`send_id`,`dest_name`,`dest_id`,`title`,`message`,`time`,`status`,"
-		"`zeny`,`amount`,`nameid`,`refine`,`attribute`,`identify`,`unique_id`,`bound`");
-	for( j = 0; j < MAX_SLOTS; j++ )
+	StringBuf_AppendStr(&buf, "SELECT `amount`,`nameid`,`refine`,`attribute`,`identify`,`unique_id`,`bound`");
+	for (j = 0; j < MAX_SLOTS; j++)
 		StringBuf_Printf(&buf, ",`card%d`", j);
 	for (j = 0; j < MAX_ITEM_RDM_OPT; ++j) {
 		StringBuf_Printf(&buf, ", `option_id%d`", j);
 		StringBuf_Printf(&buf, ", `option_val%d`", j);
 		StringBuf_Printf(&buf, ", `option_parm%d`", j);
 	}
-	StringBuf_Printf(&buf, " FROM `%s` WHERE `id` = '%d'", schema_config.mail_db, mail_id);
+	StringBuf_Printf(&buf, " FROM `%s`", schema_config.mail_attachment_db);
+	StringBuf_Printf(&buf, " WHERE `id` = '%d'", mail_id);
+	StringBuf_AppendStr(&buf, " ORDER BY `index` ASC");
+	StringBuf_Printf(&buf, " LIMIT %d", MAIL_MAX_ITEM);
 
-	if( SQL_ERROR == Sql_Query(sql_handle, StringBuf_Value(&buf))
-	||  SQL_SUCCESS != Sql_NextRow(sql_handle) )
-	{
+	if( SQL_ERROR == Sql_Query(sql_handle, StringBuf_Value(&buf)) ){
 		Sql_ShowDebug(sql_handle);
 		Sql_FreeResult(sql_handle);
 		StringBuf_Destroy(&buf);
 		return false;
 	}
-	else
-	{
-		char* data;
 
-		Sql_GetData(sql_handle, 0, &data, NULL); msg->id = atoi(data);
-		Sql_GetData(sql_handle, 1, &data, NULL); safestrncpy(msg->send_name, data, NAME_LENGTH);
-		Sql_GetData(sql_handle, 2, &data, NULL); msg->send_id = atoi(data);
-		Sql_GetData(sql_handle, 3, &data, NULL); safestrncpy(msg->dest_name, data, NAME_LENGTH);
-		Sql_GetData(sql_handle, 4, &data, NULL); msg->dest_id = atoi(data);
-		Sql_GetData(sql_handle, 5, &data, NULL); safestrncpy(msg->title, data, MAIL_TITLE_LENGTH);
-		Sql_GetData(sql_handle, 6, &data, NULL); safestrncpy(msg->body, data, MAIL_BODY_LENGTH);
-		Sql_GetData(sql_handle, 7, &data, NULL); msg->timestamp = atoi(data);
-		Sql_GetData(sql_handle, 8, &data, NULL); msg->status = (mail_status)atoi(data);
-		Sql_GetData(sql_handle, 9, &data, NULL); msg->zeny = atoi(data);
-		Sql_GetData(sql_handle,10, &data, NULL); msg->item.amount = (short)atoi(data);
-		Sql_GetData(sql_handle,11, &data, NULL); msg->item.nameid = atoi(data);
-		Sql_GetData(sql_handle,12, &data, NULL); msg->item.refine = atoi(data);
-		Sql_GetData(sql_handle,13, &data, NULL); msg->item.attribute = atoi(data);
-		Sql_GetData(sql_handle,14, &data, NULL); msg->item.identify = atoi(data);
-		Sql_GetData(sql_handle,15, &data, NULL); msg->item.unique_id = strtoull(data, NULL, 10);
-		Sql_GetData(sql_handle,16, &data, NULL); msg->item.bound = atoi(data);
-		msg->item.expire_time = 0;
-
-		for( j = 0; j < MAX_SLOTS; j++ )
-		{
-			Sql_GetData(sql_handle,17 + j, &data, NULL);
-			msg->item.card[j] = atoi(data);
+	memset(msg->item, 0, sizeof(struct item) * MAIL_MAX_ITEM);
+
+	for( i = 0; i < MAIL_MAX_ITEM && SQL_SUCCESS == Sql_NextRow(sql_handle); i++ ){
+		Sql_GetData(sql_handle,0, &data, NULL); msg->item[i].amount = (short)atoi(data);
+		Sql_GetData(sql_handle,1, &data, NULL); msg->item[i].nameid = atoi(data);
+		Sql_GetData(sql_handle,2, &data, NULL); msg->item[i].refine = atoi(data);
+		Sql_GetData(sql_handle,3, &data, NULL); msg->item[i].attribute = atoi(data);
+		Sql_GetData(sql_handle,4, &data, NULL); msg->item[i].identify = atoi(data);
+		Sql_GetData(sql_handle,5, &data, NULL); msg->item[i].unique_id = strtoull(data, NULL, 10);
+		Sql_GetData(sql_handle,6, &data, NULL); msg->item[i].bound = atoi(data);
+		msg->item[i].expire_time = 0;
+
+		for( j = 0; j < MAX_SLOTS; j++ ){
+			Sql_GetData(sql_handle,7 + j, &data, NULL); msg->item[i].card[j] = atoi(data);
 		}
-		for (j = 0; j < MAX_ITEM_RDM_OPT; j++) {
-			Sql_GetData(sql_handle, 17 + MAX_SLOTS + j * 3, &data, NULL);
-			msg->item.option[j].id = atoi(data);
-			Sql_GetData(sql_handle, 18 + MAX_SLOTS + j * 3, &data, NULL);
-			msg->item.option[j].value = atoi(data);
-			Sql_GetData(sql_handle, 19 + MAX_SLOTS + j * 3, &data, NULL);
-			msg->item.option[j].param = atoi(data);
+
+		for( j = 0; j < MAX_ITEM_RDM_OPT; j++ ){
+			Sql_GetData(sql_handle, 7 + MAX_SLOTS + j * 3, &data, NULL); msg->item[i].option[j].id = atoi(data);
+			Sql_GetData(sql_handle, 8 + MAX_SLOTS + j * 3, &data, NULL); msg->item[i].option[j].value = atoi(data);
+			Sql_GetData(sql_handle, 9 + MAX_SLOTS + j * 3, &data, NULL); msg->item[i].option[j].param = atoi(data);
 		}
 	}
 
@@ -229,27 +230,95 @@ static bool mail_loadmessage(int mail_id, struct mail_message* msg)
 	return true;
 }
 
+static int mail_timer_sub( int limit, enum mail_inbox_type type ){
+	struct{
+		int mail_id;
+		int char_id;
+		int account_id;
+	}mails[MAIL_MAX_INBOX];
+	int i, map_fd;
+	char* data;
+	struct online_char_data* character;
+
+	if( limit <= 0 ){
+		return 0;
+	}
+
+	memset(mails, 0, sizeof(mails));
+
+	if( SQL_ERROR == Sql_Query(sql_handle, "SELECT `id`,`char_id`,`account_id` FROM `%s` `m` INNER JOIN `%s` `c` ON `c`.`char_id`=`m`.`dest_id` WHERE `type` = '%d' AND `time` <= UNIX_TIMESTAMP( NOW() - INTERVAL %d DAY ) ORDER BY `id` LIMIT %d", schema_config.mail_db, schema_config.char_db, type, limit, MAIL_MAX_INBOX + 1) ){
+		Sql_ShowDebug(sql_handle);
+		return 0;
+	}
+
+	if( Sql_NumRows(sql_handle) <= 0 ){
+		return 0;
+	}
+
+	for( i = 0; i < MAIL_MAX_INBOX && SQL_SUCCESS == Sql_NextRow(sql_handle); i++ ){
+		Sql_GetData(sql_handle, 0, &data, NULL); mails[i].mail_id = atoi(data);
+		Sql_GetData(sql_handle, 1, &data, NULL); mails[i].char_id = atoi(data);
+		Sql_GetData(sql_handle, 2, &data, NULL); mails[i].account_id = atoi(data);
+	}
+
+	Sql_FreeResult(sql_handle);
+
+	for( i = 0; i < MAIL_MAX_INBOX; i++ ){
+		if( mails[i].mail_id == 0 ){
+			break;
+		}
+
+		// Check for online players
+		if( ( character = (struct online_char_data*)idb_get(char_get_onlinedb(), mails[i].account_id) ) != NULL && character->server >= 0 ){
+			map_fd = map_server[character->server].fd;
+		}else{
+			map_fd = 0;
+		}
+
+		if( type == MAIL_INBOX_NORMAL ){
+			mapif_Mail_return( 0, mails[i].char_id, mails[i].mail_id );
+			mapif_Mail_delete( map_fd, mails[i].char_id, mails[i].mail_id, true );
+		}else if( type == MAIL_INBOX_RETURNED ){
+			mapif_Mail_delete( map_fd, mails[i].char_id, mails[i].mail_id, false );
+		}else{
+			// Should not happen
+			continue;
+		}
+	}
+
+	return 0;
+}
+
+int mail_return_timer( int tid, unsigned int tick, int id, intptr_t ptr ){
+	return mail_timer_sub( charserv_config.mail_return_days, MAIL_INBOX_NORMAL );
+}
+
+int mail_delete_timer( int tid, unsigned int tick, int id, intptr_t data ){
+	return mail_timer_sub( charserv_config.mail_delete_days, MAIL_INBOX_RETURNED );
+}
+
 /*==========================================
  * Client Inbox Request
  *------------------------------------------*/
-static void mapif_Mail_sendinbox(int fd, uint32 char_id, unsigned char flag)
+static void mapif_Mail_sendinbox(int fd, uint32 char_id, unsigned char flag, enum mail_inbox_type type)
 {
 	struct mail_data md;
 	mail_fromsql(char_id, &md);
 
 	//FIXME: dumping the whole structure like this is unsafe [ultramage]
-	WFIFOHEAD(fd, sizeof(md) + 9);
+	WFIFOHEAD(fd, sizeof(md) + 10);
 	WFIFOW(fd,0) = 0x3848;
-	WFIFOW(fd,2) = sizeof(md) + 9;
+	WFIFOW(fd,2) = sizeof(md) + 10;
 	WFIFOL(fd,4) = char_id;
 	WFIFOB(fd,8) = flag;
-	memcpy(WFIFOP(fd,9),&md,sizeof(md));
+	WFIFOB(fd,9) = type;
+	memcpy(WFIFOP(fd,10),&md,sizeof(md));
 	WFIFOSET(fd,WFIFOW(fd,2));
 }
 
 static void mapif_parse_Mail_requestinbox(int fd)
 {
-	mapif_Mail_sendinbox(fd, RFIFOL(fd,2), RFIFOB(fd,6));
+	mapif_Mail_sendinbox(fd, RFIFOL(fd,2), RFIFOB(fd,6), RFIFOB(fd,7));
 }
 
 /*==========================================
@@ -265,38 +334,23 @@ static void mapif_parse_Mail_read(int fd)
 /*==========================================
  * Client Attachment Request
  *------------------------------------------*/
-static bool mail_DeleteAttach(int mail_id)
-{
-	StringBuf buf;
-	int i;
-
-	StringBuf_Init(&buf);
-	StringBuf_Printf(&buf, "UPDATE `%s` SET `zeny` = '0', `nameid` = '0', `amount` = '0', `refine` = '0', `attribute` = '0', `identify` = '0'", schema_config.mail_db);
-	for (i = 0; i < MAX_SLOTS; i++)
-		StringBuf_Printf(&buf, ", `card%d` = '0'", i);
-	for (i = 0; i < MAX_ITEM_RDM_OPT; ++i) {
-		StringBuf_Printf(&buf, ", `option_id%d` = 0", i);
-		StringBuf_Printf(&buf, ", `option_val%d` = 0", i);
-		StringBuf_Printf(&buf, ", `option_parm%d` = 0", i);
-	}
-	StringBuf_Printf(&buf, " WHERE `id` = '%d'", mail_id);
-
-	if( SQL_ERROR == Sql_Query(sql_handle, StringBuf_Value(&buf)) )
-	{
+static bool mail_DeleteAttach(int mail_id){
+	if( SQL_ERROR == Sql_Query(sql_handle, "DELETE FROM `%s` WHERE `id` = '%d'", schema_config.mail_attachment_db, mail_id ) ){
 		Sql_ShowDebug(sql_handle);
-		StringBuf_Destroy(&buf);
-
 		return false;
 	}
 
-	StringBuf_Destroy(&buf);
 	return true;
 }
 
-static void mapif_Mail_getattach(int fd, uint32 char_id, int mail_id)
+static void mapif_Mail_getattach(int fd, uint32 char_id, int mail_id, int type)
 {
 	struct mail_message msg;
 
+	if( ( type&MAIL_ATT_ALL ) == 0 ){
+		return;
+	}
+
 	if( !mail_loadmessage(mail_id, &msg) )
 		return;
 
@@ -306,36 +360,77 @@ static void mapif_Mail_getattach(int fd, uint32 char_id, int mail_id)
 	if( msg.status != MAIL_READ )
 		return;
 
-	if( (msg.item.nameid < 1 || msg.item.amount < 1) && msg.zeny < 1 )
-		return; // No Attachment
+	if( type & MAIL_ATT_ZENY ){
+		if( msg.zeny > 0 ){
+			if( SQL_ERROR == Sql_Query(sql_handle, "UPDATE `%s` SET `zeny` = 0 WHERE `id` = '%d'", schema_config.mail_db, mail_id ) ){
+				Sql_ShowDebug(sql_handle);
+				return;
+			}
+		}else{
+			type &= ~MAIL_ATT_ZENY;
+		}
+	}
 
-	if( !mail_DeleteAttach(mail_id) )
-		return;
+	if( type & MAIL_ATT_ITEM ){
+		int i;
+
+		ARR_FIND(0, MAIL_MAX_ITEM, i, msg.item[i].nameid > 0 && msg.item[i].amount > 0);
+
+		// No item was found
+		if( i == MAIL_MAX_ITEM ){
+			type &= ~MAIL_ATT_ITEM;
+		}else{
+			if( !mail_DeleteAttach(mail_id) ){
+				return;
+			}
+		}
+	}
 
-	WFIFOHEAD(fd, sizeof(struct item) + 12);
+	if( type == MAIL_ATT_NONE )
+		return; // No Attachment
+
+	WFIFOHEAD(fd, sizeof(struct item)*MAIL_MAX_ITEM + 16);
 	WFIFOW(fd,0) = 0x384a;
-	WFIFOW(fd,2) = sizeof(struct item) + 12;
+	WFIFOW(fd,2) = sizeof(struct item)*MAIL_MAX_ITEM + 16;
 	WFIFOL(fd,4) = char_id;
-	WFIFOL(fd,8) = (msg.zeny > 0)?msg.zeny:0;
-	memcpy(WFIFOP(fd,12), &msg.item, sizeof(struct item));
+	WFIFOL(fd,8) = mail_id;
+	if( type & MAIL_ATT_ZENY ){
+		WFIFOL(fd,12) = msg.zeny;
+	}else{
+		WFIFOL(fd, 12) = 0;
+	}
+	if( type & MAIL_ATT_ITEM ){
+		memcpy(WFIFOP(fd, 16), &msg.item, sizeof(struct item)*MAIL_MAX_ITEM);
+	}else{
+		memset(WFIFOP(fd, 16), 0, sizeof(struct item)*MAIL_MAX_ITEM);
+	}
 	WFIFOSET(fd,WFIFOW(fd,2));
 }
 
 static void mapif_parse_Mail_getattach(int fd)
 {
-	mapif_Mail_getattach(fd, RFIFOL(fd,2), RFIFOL(fd,6));
+	mapif_Mail_getattach(fd, RFIFOL(fd,2), RFIFOL(fd,6),RFIFOB(fd,10));
 }
 
 /*==========================================
  * Delete Mail
  *------------------------------------------*/
-static void mapif_Mail_delete(int fd, uint32 char_id, int mail_id)
+static void mapif_Mail_delete(int fd, uint32 char_id, int mail_id, bool deleted)
 {
 	bool failed = false;
-	if ( SQL_ERROR == Sql_Query(sql_handle, "DELETE FROM `%s` WHERE `id` = '%d'", schema_config.mail_db, mail_id) )
-	{
-		Sql_ShowDebug(sql_handle);
-		failed = true;
+
+	if( !deleted ){
+		if ( SQL_ERROR == Sql_Query(sql_handle, "DELETE FROM `%s` WHERE `id` = '%d'", schema_config.mail_db, mail_id) ||
+			SQL_ERROR == Sql_Query(sql_handle, "DELETE FROM `%s` WHERE `id` = '%d'", schema_config.mail_attachment_db, mail_id) )
+		{
+			Sql_ShowDebug(sql_handle);
+			failed = true;
+		}
+	}
+
+	// Only if the request came from a map-server and was not timer triggered for an offline character
+	if( fd <= 0 ){
+		return;
 	}
 
 	WFIFOHEAD(fd,11);
@@ -348,7 +443,7 @@ static void mapif_Mail_delete(int fd, uint32 char_id, int mail_id)
 
 static void mapif_parse_Mail_delete(int fd)
 {
-	mapif_Mail_delete(fd, RFIFOL(fd,2), RFIFOL(fd,6));
+	mapif_Mail_delete(fd, RFIFOL(fd,2), RFIFOL(fd,6), false);
 }
 
 /*==========================================
@@ -356,7 +451,7 @@ static void mapif_parse_Mail_delete(int fd)
  *------------------------------------------*/
 void mapif_Mail_new(struct mail_message *msg)
 {
-	unsigned char buf[74];
+	unsigned char buf[75];
 
 	if( !msg || !msg->id )
 		return;
@@ -366,7 +461,8 @@ void mapif_Mail_new(struct mail_message *msg)
 	WBUFL(buf,6) = msg->id;
 	memcpy(WBUFP(buf,10), msg->send_name, NAME_LENGTH);
 	memcpy(WBUFP(buf,34), msg->title, MAIL_TITLE_LENGTH);
-	chmapif_sendall(buf, 74);
+	WBUFB(buf,74) = msg->type;
+	chmapif_sendall(buf, 75);
 }
 
 /*==========================================
@@ -381,7 +477,8 @@ static void mapif_Mail_return(int fd, uint32 char_id, int mail_id)
 	{
 		if( msg.dest_id != char_id)
 			return;
-		else if( SQL_ERROR == Sql_Query(sql_handle, "DELETE FROM `%s` WHERE `id` = '%d'", schema_config.mail_db, mail_id) )
+		else if( SQL_ERROR == Sql_Query(sql_handle, "DELETE FROM `%s` WHERE `id` = '%d'", schema_config.mail_db, mail_id)
+			|| SQL_ERROR == Sql_Query(sql_handle, "DELETE FROM `%s` WHERE `id` = '%d'", schema_config.mail_attachment_db, mail_id) )
 			Sql_ShowDebug(sql_handle);
 		else
 		{
@@ -398,6 +495,7 @@ static void mapif_Mail_return(int fd, uint32 char_id, int mail_id)
 			safestrncpy(msg.title, temp_, MAIL_TITLE_LENGTH);
 
 			msg.status = MAIL_NEW;
+			msg.type = MAIL_INBOX_RETURNED;
 			msg.timestamp = time(NULL);
 
 			new_mail = mail_savemessage(&msg);
@@ -405,6 +503,11 @@ static void mapif_Mail_return(int fd, uint32 char_id, int mail_id)
 		}
 	}
 
+	// Only if the request came from a map-server and was not timer triggered for an offline character
+	if( fd <= 0 ){
+		return;
+	}
+
 	WFIFOHEAD(fd,11);
 	WFIFOW(fd,0) = 0x384c;
 	WFIFOL(fd,2) = char_id;
@@ -452,12 +555,18 @@ static void mapif_parse_Mail_send(int fd)
 	if ( SQL_SUCCESS == Sql_NextRow(sql_handle) )
 	{
 		char *data;
+#if PACKETVER < 20150513
 		Sql_GetData(sql_handle, 0, &data, NULL);
 		if (atoi(data) != account_id)
 		{ // Cannot send mail to char in the same account
 			Sql_GetData(sql_handle, 1, &data, NULL);
 			msg.dest_id = atoi(data);
 		}
+#else
+		// In RODEX you can even send mails to yourself
+		Sql_GetData(sql_handle, 1, &data, NULL);
+		msg.dest_id = atoi(data);
+#endif
 	}
 	Sql_FreeResult(sql_handle);
 	msg.status = MAIL_NEW;
@@ -469,7 +578,7 @@ static void mapif_parse_Mail_send(int fd)
 	mapif_Mail_new(&msg); // notify recipient
 }
 
-void mail_sendmail(int send_id, const char* send_name, int dest_id, const char* dest_name, const char* title, const char* body, int zeny, struct item *item)
+void mail_sendmail(int send_id, const char* send_name, int dest_id, const char* dest_name, const char* title, const char* body, int zeny, struct item *item, int amount)
 {
 	struct mail_message msg;
 	memset(&msg, 0, sizeof(struct mail_message));
@@ -481,15 +590,56 @@ void mail_sendmail(int send_id, const char* send_name, int dest_id, const char*
 	safestrncpy(msg.title, title, MAIL_TITLE_LENGTH);
 	safestrncpy(msg.body, body, MAIL_BODY_LENGTH);
 	msg.zeny = zeny;
-	if( item != NULL )
-		memcpy(&msg.item, item, sizeof(struct item));
+	if( item != NULL ){
+		int i;
+
+		for( i = 0; i < amount && i < MAIL_MAX_ITEM; i++ ){
+			memcpy(&msg.item[i], &item[i], sizeof(struct item));
+		}
+	}
 
 	msg.timestamp = time(NULL);
+	msg.type = MAIL_INBOX_NORMAL;
 
 	mail_savemessage(&msg);
 	mapif_Mail_new(&msg);
 }
 
+static void mapif_Mail_receiver_send( int fd, int requesting_char_id, int char_id, int class_, int base_level, const char* name ){
+	WFIFOHEAD(fd,38);
+	WFIFOW(fd,0) = 0x384e;
+	WFIFOL(fd,2) = requesting_char_id;
+	WFIFOL(fd,6) = char_id;
+	WFIFOW(fd,10) = class_;
+	WFIFOW(fd,12) = base_level;
+	strncpy(WFIFOCP(fd, 14), name, NAME_LENGTH);
+	WFIFOSET(fd,38);
+}
+
+static void mapif_parse_Mail_receiver_check( int fd ){
+	char name[NAME_LENGTH], esc_name[NAME_LENGTH * 2 + 1];
+	uint32 char_id = 0;
+	uint16 class_ = 0, base_level = 0;
+
+	safestrncpy( name, RFIFOCP(fd, 6), NAME_LENGTH );
+
+	// Try to find the Dest Char by Name
+	Sql_EscapeStringLen( sql_handle, esc_name, name, strnlen( name, NAME_LENGTH ) );
+
+	if( SQL_ERROR == Sql_Query( sql_handle, "SELECT `char_id`,`class`,`base_level` FROM `%s` WHERE `name` = '%s'", schema_config.char_db, esc_name ) ){
+		Sql_ShowDebug(sql_handle);
+	}else if( SQL_SUCCESS == Sql_NextRow(sql_handle) ){
+		char *data;
+
+		Sql_GetData(sql_handle, 0, &data, NULL); char_id = atoi(data);
+		Sql_GetData(sql_handle, 1, &data, NULL); class_ = atoi(data);
+		Sql_GetData(sql_handle, 2, &data, NULL); base_level = atoi(data);
+	}
+
+	Sql_FreeResult(sql_handle);
+	mapif_Mail_receiver_send( fd, RFIFOL(fd, 2), char_id, class_, base_level, name );
+}
+
 /*==========================================
  * Packets From Map Server
  *------------------------------------------*/
@@ -503,6 +653,7 @@ int inter_mail_parse_frommap(int fd)
 		case 0x304b: mapif_parse_Mail_delete(fd); break;
 		case 0x304c: mapif_parse_Mail_return(fd); break;
 		case 0x304d: mapif_parse_Mail_send(fd); break;
+		case 0x304e: mapif_parse_Mail_receiver_check(fd); break;
 		default:
 			return 0;
 	}

+ 4 - 1
src/char/int_mail.h

@@ -4,8 +4,11 @@
 #ifndef _INT_MAIL_SQL_H_
 #define _INT_MAIL_SQL_H_
 
+int mail_return_timer( int tid, unsigned int tick, int id, intptr_t data );
+int mail_delete_timer( int tid, unsigned int tick, int id, intptr_t data );
+
 int inter_mail_parse_frommap(int fd);
-void mail_sendmail(int send_id, const char* send_name, int dest_id, const char* dest_name, const char* title, const char* body, int zeny, struct item *item);
+void mail_sendmail(int send_id, const char* send_name, int dest_id, const char* dest_name, const char* title, const char* body, int zeny, struct item *item, int amount);
 
 int inter_mail_sql_init(void);
 void inter_mail_sql_final(void);

+ 1 - 1
src/char/inter.c

@@ -49,7 +49,7 @@ int inter_recv_packet_length[] = {
 	 6,-1, 0, 0,  0, 0, 0, 0, 10,-1, 0, 0,  0, 0,  0, 0,	// 3010-
 	-1,10,-1,14, 15+NAME_LENGTH,19, 6,-1, 14,14, 6, 0,  0, 0,  0, 0,	// 3020- Party
 	-1, 6,-1,-1, 55,19, 6,-1, 14,-1,-1,-1, 18,19,186,-1,	// 3030-
-	-1, 9, 0, 0,  0, 0, 0, 0,  7, 6,10,10, 10,-1,  0, 0,	// 3040-
+	-1, 9, 0, 0,  0, 0, 0, 0,  8, 6,11,10, 10,-1,6+NAME_LENGTH, 0,	// 3040-
 	-1,-1,10,10,  0,-1,12, 0,  0, 0, 0, 0,  0, 0,  0, 0,	// 3050-  Auction System [Zephyrus]
 	 6,-1, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0,  0, 0,	// 3060-  Quest system [Kevin] [Inkfish]
 	-1,10, 6,-1,  0, 0, 0, 0,  0, 0, 0, 0, -1,10,  6,-1,	// 3070-  Mercenary packets [Zephyrus], Elemental packets [pakpil]

+ 23 - 1
src/common/mmo.h

@@ -140,7 +140,14 @@
 //Mail System
 #define MAIL_MAX_INBOX 30
 #define MAIL_TITLE_LENGTH 40
+#if PACKETVER < 20150513
 #define MAIL_BODY_LENGTH 200
+#define MAIL_MAX_ITEM 1
+#else
+#define MAIL_BODY_LENGTH 500
+#define MAIL_MAX_ITEM 5
+#define MAIL_PAGE_SIZE 7
+#endif
 
 //Mercenary System
 #define MC_SKILLBASE 8201
@@ -500,6 +507,19 @@ typedef enum mail_status {
 	MAIL_READ,
 } mail_status;
 
+enum mail_inbox_type {
+	MAIL_INBOX_NORMAL = 0,
+	MAIL_INBOX_ACCOUNT,
+	MAIL_INBOX_RETURNED
+};
+
+enum mail_attachment_type {
+	MAIL_ATT_NONE = 0,
+	MAIL_ATT_ZENY = 1,
+	MAIL_ATT_ITEM = 2,
+	MAIL_ATT_ALL = MAIL_ATT_ZENY | MAIL_ATT_ITEM
+};
+
 struct mail_message {
 	int id;
 	uint32 send_id;                 //hold char_id of sender
@@ -508,12 +528,14 @@ struct mail_message {
 	char dest_name[NAME_LENGTH];    //receiver nickname
 	char title[MAIL_TITLE_LENGTH];
 	char body[MAIL_BODY_LENGTH];
+	int type; // enum mail_inbox_type
+	time_t scheduled_deletion;
 
 	mail_status status;
 	time_t timestamp; // marks when the message was sent
 
 	uint32 zeny;
-	struct item item;
+	struct item item[MAIL_MAX_ITEM];
 };
 
 struct mail_data {

+ 4 - 0
src/map/battle.c

@@ -8407,6 +8407,10 @@ static const struct _battle_data {
 	{ "dispel_song",                        &battle_config.dispel_song,                     0,      0,      1,              },
 	{ "guild_maprespawn_clones",			&battle_config.guild_maprespawn_clones,			0,		0,		1,				},
 	{ "hide_fav_sell", 			&battle_config.hide_fav_sell,			0,      0,      1,              },
+	{ "mail_daily_count",					&battle_config.mail_daily_count,				100,	0,		INT32_MAX,		},
+	{ "mail_zeny_fee",						&battle_config.mail_zeny_fee,					2,		0,		100,			},
+	{ "mail_attachment_price",				&battle_config.mail_attachment_price,			2500,	0,		INT32_MAX,		},
+	{ "mail_attachment_weight",				&battle_config.mail_attachment_weight,			2000,	0,		INT32_MAX,		},
 
 #include "../custom/battle_config_init.inc"
 };

+ 4 - 0
src/map/battle.h

@@ -618,6 +618,10 @@ extern struct Battle_Config
 	int dispel_song; //Can songs be dispelled?
 	int guild_maprespawn_clones; // Should clones be killed by maprespawnguildid?
 	int hide_fav_sell;
+	int mail_daily_count;
+	int mail_zeny_fee;
+	int mail_attachment_price;
+	int mail_attachment_weight;
 
 #include "../custom/battle_config_struct.inc"
 } battle_config;

+ 639 - 105
src/map/clif.c

@@ -72,6 +72,15 @@ static unsigned int clif_cryptKey[3]; // Used keys
 static unsigned short clif_parse_cmd(int fd, struct map_session_data *sd);
 static bool clif_session_isValid(struct map_session_data *sd);
 
+#if PACKETVER >= 20150513
+enum mail_type {
+	MAIL_TYPE_TEXT = 0x0,
+	MAIL_TYPE_ZENY = 0x2,
+	MAIL_TYPE_ITEM = 0x4,
+	MAIL_TYPE_NPC = 0x8
+};
+#endif
+
 /** Converts item type to display it on client if necessary.
 * @param nameid: Item ID
 * @return item type. For IT_PETEGG will be displayed as IT_WEAPON. If Shadow Weapon of IT_SHADOWGEAR as IT_WEAPON and else as IT_ARMOR
@@ -10538,6 +10547,12 @@ void clif_parse_LoadEndAck(int fd,struct map_session_data *sd)
 	pc_show_questinfo_reinit(sd);
 	pc_show_questinfo(sd);
 
+#if PACKETVER >= 20150513
+	if( sd->mail.inbox.unread ){
+		clif_Mail_new(sd, 0, NULL, NULL);
+	}
+#endif
+
 	sd->state.connect_new = 0;
 	sd->state.changemap = false;
 }
@@ -11438,6 +11453,8 @@ void clif_parse_NpcClicked(int fd,struct map_session_data *sd)
 #endif
 	if (pc_cant_act2(sd) || sd->npc_id)
 		return;
+	if( sd->state.mail_writing )
+		return;
 
 	bl = map_id2bl(RFIFOL(fd,info->pos[0]));
 	//type = RFIFOB(fd,info->pos[1]);
@@ -11681,10 +11698,24 @@ void clif_parse_TradeRequest(int fd,struct map_session_data *sd)
 	if(!sd->chatID && pc_cant_act(sd))
 		return; //You can trade while in a chatroom.
 
-	// @noask [LuzZza]
-	if(t_sd && t_sd->state.noask) {
-		clif_noask_sub(sd, t_sd, 0);
-		return;
+	if(t_sd){
+		// @noask [LuzZza]
+		if(t_sd->state.noask) {
+			clif_noask_sub(sd, t_sd, 0);
+			return;
+		}
+
+		if (t_sd->state.mail_writing) {
+			int old = sd->trade_partner;
+
+			// Fake trading
+			sd->trade_partner = t_sd->status.account_id;
+			clif_tradestart(sd, 5);
+			// Restore old state
+			sd->trade_partner = old;
+
+			return;
+		}
 	}
 
 	if( battle_config.basic_skill_check && pc_checkskill(sd,NV_BASIC) < 1 && pc_checkskill(sd, SU_BASIC_SKILL) < 1) {
@@ -14759,62 +14790,144 @@ void clif_parse_Check(int fd, struct map_session_data *sd)
 /// By Zephyrus
 ///
 
-/// Notification about the result of adding an item to mail (ZC_ACK_MAIL_ADD_ITEM).
-/// 0255 <index>.W <result>.B
+/// Notification about the result of adding an item to mail
+/// 0255 <index>.W <result>.B (ZC_ACK_MAIL_ADD_ITEM)
 /// result:
 ///     0 = success
 ///     1 = failure
-void clif_Mail_setattachment(int fd, int index, uint8 flag)
+/// 0a05 <result>.B <index>.W <amount>.W <item id>.W <item type>.B <identified>.B <damaged>.B <refine>.B (ZC_ACK_ADD_ITEM_TO_MAIL)
+///		{ <card id>.W }*4 { <option index>.W <option value>.W <option parameter>.B }*5
+/// result:
+///		0 = success
+///		1 = over weight
+///		2 = fatal error
+void clif_Mail_setattachment(struct map_session_data* sd, int index, int amount, uint8 flag)
 {
+	int fd = sd->fd;
+
+#if PACKETVER < 20150513
 	WFIFOHEAD(fd,packet_len(0x255));
 	WFIFOW(fd,0) = 0x255;
 	WFIFOW(fd,2) = index;
 	WFIFOB(fd,4) = flag;
 	WFIFOSET(fd,packet_len(0x255));
+#else
+	struct item* item = &sd->inventory.u.items_inventory[index-2];
+	int i, total = 0;
+
+	WFIFOHEAD(fd, 53);
+	WFIFOW(fd, 0) = 0xa05;
+	WFIFOB(fd, 2) = flag;
+	WFIFOW(fd, 3) = index;
+	WFIFOW(fd, 5) = amount;
+	WFIFOW(fd, 7) = item->nameid;
+	WFIFOB(fd, 9) = itemtype(item->nameid);
+	WFIFOB(fd, 10) = item->identify;
+	WFIFOB(fd, 11) = item->attribute;
+	WFIFOB(fd, 12) = item->refine;
+	clif_addcards(WFIFOP(fd,13), item);
+	clif_add_random_options(WFIFOP(fd,21), item);
+
+	for( i = 0; i < MAIL_MAX_ITEM; i++ ){
+		if( sd->mail.item[i].nameid == 0 ){
+			break;
+		}
+
+		total += sd->mail.item[i].amount * ( sd->inventory_data[sd->mail.item[i].index]->weight / 10 );
+	}
+
+	WFIFOW(fd, 46) = total;
+	// 5 empty unknown bytes
+	memset(WFIFOP(fd, 48), 0, 5);
+	WFIFOSET(fd, 53);
+#endif
 }
 
 
-/// Notification about the result of retrieving a mail attachment (ZC_MAIL_REQ_GET_ITEM).
-/// 0245 <result>.B
+/// Notification about the result of retrieving a mail attachment
+/// 0245 <result>.B (ZC_MAIL_REQ_GET_ITEM)
 /// result:
 ///     0 = success
 ///     1 = failure
 ///     2 = too many items
-void clif_Mail_getattachment(int fd, uint8 result)
-{
+/// 09f2 <mail id>.Q <mail tab>.B <result>.B (ZC_ACK_ZENY_FROM_MAIL)
+/// 09f4 <mail id>.Q <mail tab>.B <result>.B (ZC_ACK_ITEM_FROM_MAIL)
+void clif_mail_getattachment(struct map_session_data* sd, struct mail_message *msg, uint8 result, enum mail_attachment_type type) {
+	int fd = sd->fd;
+
+#if PACKETVER < 20150513
 	WFIFOHEAD(fd,packet_len(0x245));
 	WFIFOW(fd,0) = 0x245;
 	WFIFOB(fd,2) = result;
 	WFIFOSET(fd,packet_len(0x245));
+#else
+	switch( type ){
+		case MAIL_ATT_ITEM:
+		case MAIL_ATT_ZENY:
+			break;
+		default:
+			return;
+	}
+
+	WFIFOHEAD(fd, 12);
+	WFIFOW(fd, 0) = type == MAIL_ATT_ZENY ? 0x9f2 : 0x9f4;
+	WFIFOQ(fd, 2) = msg->id;
+	WFIFOB(fd, 10) = msg->type;
+	WFIFOB(fd, 11) = result;
+	WFIFOSET(fd, 12);
+
+	clif_Mail_refreshinbox( sd, msg->type, 0 );
+#endif
 }
 
 
-/// Notification about the result of sending a mail (ZC_MAIL_REQ_SEND).
-/// 0249 <result>.B
+/// Notification about the result of sending a mail
+/// 0249 <result>.B (ZC_MAIL_REQ_SEND)
 /// result:
 ///     0 = success
 ///     1 = recipinent does not exist
-void clif_Mail_send(int fd, bool fail)
-{
+/// 09ed <result>.B (ZC_ACK_WRITE_MAIL)
+void clif_Mail_send(struct map_session_data* sd, enum mail_send_result result){
+	int fd = sd->fd;
+#if PACKETVER < 20150513
 	WFIFOHEAD(fd,packet_len(0x249));
 	WFIFOW(fd,0) = 0x249;
-	WFIFOB(fd,2) = fail;
+	WFIFOB(fd,2) = result != WRITE_MAIL_SUCCESS;
 	WFIFOSET(fd,packet_len(0x249));
+#else
+	WFIFOHEAD(fd, 3);
+	WFIFOW(fd, 0) = 0x9ed;
+	WFIFOB(fd, 2) = result;
+	WFIFOSET(fd, 3);
+#endif
 }
 
 
-/// Notification about the result of deleting a mail (ZC_ACK_MAIL_DELETE).
-/// 0257 <mail id>.L <result>.W
+/// Notification about the result of deleting a mail.
+/// 0257 <mail id>.L <result>.W (ZC_ACK_MAIL_DELETE)
 /// result:
 ///     0 = success
 ///     1 = failure
-void clif_Mail_delete(int fd, int mail_id, short fail)
-{
+// 09f6 <mail tab>.B <mail id>.Q (ZC_ACK_DELETE_MAIL)
+void clif_mail_delete( struct map_session_data* sd, struct mail_message *msg, bool success ){
+	int fd = sd->fd;
+
+#if PACKETVER < 20150513
 	WFIFOHEAD(fd, packet_len(0x257));
 	WFIFOW(fd,0) = 0x257;
-	WFIFOL(fd,2) = mail_id;
-	WFIFOW(fd,6) = fail;
+	WFIFOL(fd,2) = msg->id;
+	WFIFOW(fd,6) = !success;
 	WFIFOSET(fd, packet_len(0x257));
+#else
+	// This is only a success notification
+	if( success ){
+		WFIFOHEAD(fd, 11);
+		WFIFOW(fd, 0) = 0x9f6;
+		WFIFOB(fd, 2) = msg->type;
+		WFIFOQ(fd, 3) = msg->id;
+		WFIFOSET(fd, 11);
+	}
+#endif
 }
 
 
@@ -14832,20 +14945,26 @@ void clif_Mail_return(int fd, int mail_id, short fail)
 	WFIFOSET(fd,packet_len(0x274));
 }
 
-
-/// Notification about new mail (ZC_MAIL_RECEIVE).
-/// 024a <mail id>.L <title>.40B <sender>.24B
-void clif_Mail_new(int fd, int mail_id, const char *sender, const char *title)
-{
+/// Notification about new mail.
+/// 024a <mail id>.L <title>.40B <sender>.24B (ZC_MAIL_RECEIVE)
+/// 09e7 <result>.B (ZC_NOTIFY_UNREADMAIL)
+void clif_Mail_new(struct map_session_data* sd, int mail_id, const char *sender, const char *title){
+	int fd = sd->fd;
+#if PACKETVER < 20150513
 	WFIFOHEAD(fd,packet_len(0x24a));
 	WFIFOW(fd,0) = 0x24a;
 	WFIFOL(fd,2) = mail_id;
 	safestrncpy(WFIFOCP(fd,6), title, MAIL_TITLE_LENGTH);
 	safestrncpy(WFIFOCP(fd,46), sender, NAME_LENGTH);
 	WFIFOSET(fd,packet_len(0x24a));
+#else
+	WFIFOHEAD(fd,3);
+	WFIFOW(fd,0) = 0x9e7;
+	WFIFOB(fd,2) = sd->mail.inbox.unread > 0 || sd->mail.inbox.unchecked > 0; // unread
+	WFIFOSET(fd,3);
+#endif
 }
 
-
 /// Opens/closes the mail window (ZC_MAIL_WINDOWS).
 /// 0260 <type>.L
 /// type:
@@ -14859,14 +14978,17 @@ void clif_Mail_window(int fd, int flag)
 	WFIFOSET(fd,packet_len(0x260));
 }
 
-
-/// Lists mails stored in inbox (ZC_MAIL_REQ_GET_LIST).
-/// 0240 <packet len>.W <amount>.L { <mail id>.L <title>.40B <read>.B <sender>.24B <time>.L }*amount
+/// Lists mails stored in inbox.
+/// 0240 <packet len>.W <amount>.L { <mail id>.L <title>.40B <read>.B <sender>.24B <time>.L }*amount (ZC_MAIL_REQ_GET_LIST)
 /// read:
 ///     0 = unread
 ///     1 = read
-void clif_Mail_refreshinbox(struct map_session_data *sd)
-{
+/// 09f0 <packet len>.W <type>.B <amount>.B <last page>.B (ZC_ACK_MAIL_LIST)
+///		{ <mail id>.Q <read>.B <type>.B <sender>.24B <received>.L <expires>.L <title length>.W <title>.?B }*
+/// 0a7d <packet len>.W <type>.B <amount>.B <last page>.B (ZC_ACK_MAIL_LIST2)
+///		{ <mail id>.Q <read>.B <type>.B <sender>.24B <received>.L <expires>.L <title length>.W <title>.?B }*
+void clif_Mail_refreshinbox(struct map_session_data *sd,enum mail_inbox_type type,int64 mailID){
+#if PACKETVER < 20150513
 	int fd = sd->fd;
 	struct mail_data *md = &sd->mail.inbox;
 	struct mail_message *msg;
@@ -14898,29 +15020,176 @@ void clif_Mail_refreshinbox(struct map_session_data *sd)
 		safesnprintf(output,sizeof(output),"Inbox is full (Max %d). Delete some mails.", MAIL_MAX_INBOX);
 		clif_messagecolor(&sd->bl, color_table[COLOR_LIGHT_GREEN], output, false, SELF);
 	}
-}
+#else
+	int fd = sd->fd;
+	struct mail_data *md = &sd->mail.inbox;
+	struct mail_message *msg;
+	int i, j, k, offset, titleLength;
+	uint8 mailType, amount, remaining;
+	uint32 now = (uint32)time(NULL);
+#if PACKETVER >= 20160601
+	int cmd = 0xa7d;
+#else
+	int cmd = 0x9f0;
+#endif
 
+	if( battle_config.mail_daily_count ){
+		mail_refresh_remaining_amount(sd);
+	}
 
-/// Mail inbox list request (CZ_MAIL_GET_LIST).
-/// 023f
-void clif_parse_Mail_refreshinbox(int fd, struct map_session_data *sd)
-{
+	// If a starting mail id was sent
+	if( mailID != 0 ){
+		ARR_FIND( 0, md->amount, i, md->msg[i].id == mailID );
+
+		// Unknown mail
+		if( i == md->amount ){
+			// Ignore the request for now
+			return; // TODO: Should we just show the first page instead?
+		}
+
+		// It was actually the oldest/first mail, there is no further page
+		if( i == 0 ){
+			return;
+		}
+
+		// We actually want the next/older mail
+		i -= 1;
+	}else{
+		i = md->amount;
+	}
+	
+	// Count the remaining mails from the starting mail or the beginning
+	// Only count mails of the target type and those that should not have been deleted already
+	for( j = i, remaining = 0; j >= 0; j-- ){
+		msg = &md->msg[j];
+
+		if (msg->id < 1)
+			continue;
+		if (msg->type != type)
+			continue;
+		if (msg->scheduled_deletion > 0 && msg->scheduled_deletion <= now)
+			continue;
+
+		remaining++;
+	}
+
+	if( remaining > MAIL_PAGE_SIZE ){
+		amount = MAIL_PAGE_SIZE;
+	}else{
+		amount = remaining;
+	}
+
+	WFIFOHEAD(fd, 7 + ((44 + MAIL_TITLE_LENGTH) * amount));
+	WFIFOW(fd, 0) = cmd;
+	WFIFOB(fd, 4) = type;
+	WFIFOB(fd, 5) = amount;
+	WFIFOB(fd, 6) = ( remaining < MAIL_PAGE_SIZE ); // last page
+
+	for( offset = 7, amount = 0; i >= 0; i-- ){
+		msg = &md->msg[i];
+
+		if (msg->id < 1)
+			continue;
+		if (msg->type != type)
+			continue;
+		if (msg->scheduled_deletion > 0 && msg->scheduled_deletion <= now)
+			continue;
+
+		WFIFOQ(fd, offset + 0) = (uint64)msg->id;
+		WFIFOB(fd, offset + 8) = (msg->status != MAIL_UNREAD);
+
+		mailType = MAIL_TYPE_TEXT; // Normal letter
+
+		if( msg->zeny > 0 ){
+			mailType |= MAIL_TYPE_ZENY; // Mail contains zeny
+		}
+
+		for( k = 0; k < MAIL_MAX_ITEM; k++ ){
+			if( msg->item[k].nameid > 0 ){
+				mailType |= MAIL_TYPE_ITEM; // Mail contains items
+				break;
+			}
+		}
+
+		// If it came from an npc?
+		//mailType |= MAIL_TYPE_NPC;
+
+		WFIFOB(fd, offset + 9) = mailType;
+		safestrncpy(WFIFOCP(fd, offset + 10), msg->send_name, NAME_LENGTH);
+
+		// How much time has passed since you received the mail
+		WFIFOL(fd, offset + 34 ) = now - (uint32)msg->timestamp;
+
+		// If automatic return/deletion of mails is enabled, notify the client when it will kick in
+		if( msg->scheduled_deletion > 0 ){
+			WFIFOL(fd, offset + 38) = (uint32)msg->scheduled_deletion - now;
+		}else{
+			// Fake the scheduled deletion to one year in the future
+			// Sadly the client always displays the scheduled deletion after 24 hours no matter how high this value gets [Lemongrass]
+			WFIFOL(fd, offset + 38) = 365 * 24 * 60 * 60;
+		}
+
+		WFIFOW(fd, offset + 42) = titleLength = (int16)(strlen(msg->title) + 1);
+		safestrncpy(WFIFOCP(fd, offset + 44), msg->title, titleLength);
+
+		offset += 44 + titleLength;
+	}
+	WFIFOW(fd, 2) = (int16)offset;
+	WFIFOSET(fd, offset);
+#endif
+}
+
+/// Mail inbox list request.
+/// 023f (CZ_MAIL_GET_LIST)
+/// 09e8 <mail tab>.B <mail id>.Q (CZ_OPEN_MAILBOX)
+/// 09ee <mail tab>.B <mail id>.Q (CZ_REQ_NEXT_MAIL_LIST)
+/// 09ef <mail tab>.B <mail id>.Q (CZ_REQ_REFRESH_MAIL_LIST)
+void clif_parse_Mail_refreshinbox(int fd, struct map_session_data *sd){
+#if PACKETVER < 20150513
 	struct mail_data* md = &sd->mail.inbox;
 
 	if( md->amount < MAIL_MAX_INBOX && (md->full || sd->mail.changed) )
-		intif_Mail_requestinbox(sd->status.char_id, 1);
+		intif_Mail_requestinbox(sd->status.char_id, 1, MAIL_INBOX_NORMAL);
 	else
-		clif_Mail_refreshinbox(sd);
+		clif_Mail_refreshinbox(sd, MAIL_INBOX_NORMAL,0);
 
-	mail_removeitem(sd, 0);
-	mail_removezeny(sd, 0);
-}
+	mail_removeitem(sd, 0, sd->mail.item[0].index, sd->mail.item[0].amount);
+	mail_removezeny(sd, false);
+#else
+	uint8 openType = RFIFOB(fd, 2);
+	uint64 mailId = RFIFOQ(fd, 3);
 
+	switch( openType ){
+		case MAIL_INBOX_NORMAL:
+		case MAIL_INBOX_ACCOUNT:
+		case MAIL_INBOX_RETURNED:
+			break;
+		default:
+			// Unknown type: ignore request
+			return;
+	}
 
-/// Opens a mail (ZC_MAIL_REQ_OPEN).
-/// 0242 <packet len>.W <mail id>.L <title>.40B <sender>.24B <time>.L <zeny>.L
+	if( sd->mail.changed || RFIFOW(fd,0) == 0x9ef ){
+		intif_Mail_requestinbox(sd->status.char_id, 1, openType);
+		return;
+	}
+
+	// If it is not a next page request
+	if( RFIFOW(fd,0) != 0x9ee ){
+		mailId = 0;
+	}
+
+	clif_Mail_refreshinbox(sd,(enum mail_inbox_type)openType,mailId);
+#endif
+}
+
+/// Opens a mail
+/// 0242 <packet len>.W <mail id>.L <title>.40B <sender>.24B <time>.L <zeny>.L (ZC_MAIL_REQ_OPEN)
 ///     <amount>.L <name id>.W <item type>.W <identified>.B <damaged>.B <refine>.B
 ///     <card1>.W <card2>.W <card3>.W <card4>.W <message>.?B
+/// 09eb <packet len>.W <type>.B <mail id>.Q <message length>.W <zeny>.Q <item count>.B (ZC_ACK_READ_MAIL)
+///		{  }*n
+// TODO: Packet description => for repeated block
 void clif_Mail_read(struct map_session_data *sd, int mail_id)
 {
 	int i, fd = sd->fd;
@@ -14932,15 +15201,21 @@ void clif_Mail_read(struct map_session_data *sd, int mail_id)
 		return;
 	} else {
 		struct mail_message *msg = &sd->mail.inbox.msg[i];
-		struct item *item = &msg->item;
+		struct item *item;
 		struct item_data *data;
-		int msg_len = strlen(msg->body), len;
+		int msg_len = strlen(msg->body), len, count = 0;
+#if PACKETVER >= 20150513
+		int offset, j, itemsize;
+#endif
 
 		if( msg_len == 0 ) {
-			strcpy(msg->body, "(no message)");
+			strcpy(msg->body, "(no message)"); // TODO: confirm for RODEX
 			msg_len = strlen(msg->body);
 		}
 
+#if PACKETVER < 20150513
+		item = &msg->item[0];
+
 		len = 101 + msg_len;
 
 		WFIFOHEAD(fd,len);
@@ -14969,21 +15244,72 @@ void clif_Mail_read(struct map_session_data *sd, int mail_id)
 		WFIFOB(fd,99) = (unsigned char)msg_len;
 		safestrncpy(WFIFOCP(fd,100), msg->body, msg_len + 1);
 		WFIFOSET(fd,len);
+#else
+		// Count the attached items
+		ARR_FIND( 0, MAIL_MAX_ITEM, count, msg->item[count].nameid == 0 );
+
+		msg_len += 1; // Zero Termination
+
+		itemsize = 24 + 5 * MAX_ITEM_RDM_OPT;
+		len = 24 + msg_len+1 + itemsize * count;
+
+		WFIFOHEAD(fd, len);
+		WFIFOW(fd, 0) = 0x9eb;
+		WFIFOW(fd, 2) = len;
+		WFIFOB(fd, 4) = msg->type;
+		WFIFOQ(fd, 5) = msg->id;
+		WFIFOW(fd, 13) = msg_len;
+		WFIFOQ(fd, 15) = msg->zeny;
+		WFIFOB(fd, 23) = (uint8)count; // item count
+		safestrncpy(WFIFOCP(fd, 24), msg->body, msg_len);
+
+		offset = 24 + msg_len;
+
+		for (j = 0; j < MAIL_MAX_ITEM; j++) {
+			item = &msg->item[j];
+
+			if (item->nameid > 0 && item->amount > 0 && (data = itemdb_exists(item->nameid)) != NULL) {
+				WFIFOW(fd, offset) = item->amount;
+				WFIFOW(fd, offset + 2) = (data->view_id) ? data->view_id : item->nameid;
+				WFIFOB(fd, offset + 4) = item->identify;
+				WFIFOB(fd, offset + 5) = item->attribute;
+				WFIFOB(fd, offset + 6) = item->refine;
+				clif_addcards(WFIFOP(fd, offset + 7), item);
+				// 4B unsigned long location
+				WFIFOB(fd, offset + 15 + 4) = data->type;
+				// 2B unsigned short wItemSpriteNumber
+				//WFIFOW(fd, offset + 15 + 5) = data->view_id;
+				// 2B unsigned short bindOnEquipType
+				clif_add_random_options(WFIFOP(fd, offset + 15 + 9 ), item);
+				offset += itemsize;
+			}
+		}
+
+		WFIFOSET(fd, len);
+#endif
 
 		if (msg->status == MAIL_UNREAD) {
 			msg->status = MAIL_READ;
 			intif_Mail_read(mail_id);
 			clif_parse_Mail_refreshinbox(fd, sd);
+
+			sd->mail.inbox.unread--;
+			clif_Mail_new(sd, 0, msg->send_name, msg->title);
 		}
 	}
 }
 
 
-/// Request to open a mail (CZ_MAIL_OPEN).
-/// 0241 <mail id>.L
-void clif_parse_Mail_read(int fd, struct map_session_data *sd)
-{
+/// Request to open a mail.
+/// 0241 <mail id>.L (CZ_MAIL_OPEN)
+/// 09ea <mail tab>.B <mail id>.Q (CZ_REQ_READ_MAIL)
+void clif_parse_Mail_read(int fd, struct map_session_data *sd){
+#if PACKETVER < 20150513
 	int mail_id = RFIFOL(fd,packet_db[sd->packet_ver][RFIFOW(fd,0)].pos[0]);
+#else
+	uint8 openType = RFIFOB(fd, 2);
+	int mail_id = (int)RFIFOQ(fd, 3);
+#endif
 
 	if( mail_id <= 0 )
 		return;
@@ -14993,14 +15319,91 @@ void clif_parse_Mail_read(int fd, struct map_session_data *sd)
 	clif_Mail_read(sd, mail_id);
 }
 
+/// Allow a player to begin writing a mail
+/// 0a12 <receiver>.24B <success>.B (ZC_ACK_OPEN_WRITE_MAIL)
+void clif_send_Mail_beginwrite_ack( struct map_session_data *sd, char* name, bool success ){
+	int fd = sd->fd;
 
-/// Request to receive mail's attachment (CZ_MAIL_GET_ITEM).
-/// 0244 <mail id>.L
-void clif_parse_Mail_getattach(int fd, struct map_session_data *sd)
-{
-	int mail_id = RFIFOL(fd,packet_db[sd->packet_ver][RFIFOW(fd,0)].pos[0]);
+	WFIFOHEAD(fd, 27);
+	WFIFOW(fd, 0) = 0xa12;
+	safestrncpy(WFIFOCP(fd, 2), name, NAME_LENGTH);
+	WFIFOB(fd, 26) = success;
+	WFIFOSET(fd, 27);
+}
+
+/// Request to start writing a mail
+/// 0a08 <receiver>.24B (CZ_REQ_OPEN_WRITE_MAIL)
+void clif_parse_Mail_beginwrite( int fd, struct map_session_data *sd ){
+	char name[NAME_LENGTH];
+
+	safestrncpy(name, RFIFOCP(fd, 2), NAME_LENGTH);
+
+	if( sd->state.storage_flag || sd->state.mail_writing || sd->trade_partner ){
+		clif_send_Mail_beginwrite_ack(sd, name, false);
+		return;
+	}
+
+	mail_clear(sd);
+
+	sd->state.mail_writing = true;
+	clif_send_Mail_beginwrite_ack(sd, name,true);
+}
+
+/// Notification that the client cancelled writing a mail
+/// 0a03 (CZ_REQ_CANCEL_WRITE_MAIL)
+void clif_parse_Mail_cancelwrite( int fd, struct map_session_data *sd ){
+	sd->state.mail_writing = false;
+}
+
+/// Give the client information about the recipient, if available
+/// 0a14 <char id>.L <class>.W <base level>.W (ZC_CHECK_RECEIVE_CHARACTER_NAME)
+/// 0a51 <char id>.L <class>.W <base level>.W <name>.24B (ZC_CHECK_RECEIVE_CHARACTER_NAME2)
+void clif_Mail_Receiver_Ack( struct map_session_data* sd, uint32 char_id, short class_, uint32 level, const char* name ){
+	int fd = sd->fd;
+#if PACKETVER <= 20160302
+	int cmd = 0xa14;
+#else
+	int cmd = 0xa51;
+#endif
+
+	WFIFOHEAD(fd, packet_len(cmd));
+	WFIFOW(fd, 0) = cmd;
+	WFIFOL(fd, 2) = char_id;
+	WFIFOW(fd, 6) = class_;
+	WFIFOW(fd, 8) = level;
+#if PACKETVER >= 20160302
+	strncpy(WFIFOCP(fd, 10), name, NAME_LENGTH);
+#endif
+	WFIFOSET(fd, packet_len(cmd));
+}
+
+/// Request information about the recipient
+/// 0a13 <name>.24B (CZ_CHECK_RECEIVE_CHARACTER_NAME)
+void clif_parse_Mail_Receiver_Check(int fd, struct map_session_data *sd) {
+	static char name[NAME_LENGTH];
+
+	safestrncpy(name, RFIFOCP(fd, 2), NAME_LENGTH);
+
+	intif_mail_checkreceiver(sd, name);
+}
+
+/// Request to receive mail's attachment.
+/// 0244 <mail id>.L (CZ_MAIL_GET_ITEM)
+/// 09f1 <mail id>.Q <mail tab>.B (CZ_REQ_ZENY_FROM_MAIL)
+/// 09f3 <mail id>.Q <mail tab>.B (CZ_REQ_ITEM_FROM_MAIL)
+void clif_parse_Mail_getattach( int fd, struct map_session_data *sd ){
 	int i;
 	bool fail = false;
+	struct mail_message* msg;
+#if PACKETVER < 20150513
+	int mail_id = RFIFOL(fd, packet_db[sd->packet_ver][RFIFOW(fd, 0)].pos[0]);
+	int attachment = MAIL_ATT_ALL;
+#else
+	uint16 packet_id = RFIFOW(fd, 0);
+	int mail_id = (int)RFIFOQ(fd, 2);
+	//int openType = RFIFOB(fd, 10);
+	int attachment = packet_id == 0x9f1 ? MAIL_ATT_ZENY : packet_id == 0x9f3 ? MAIL_ATT_ITEM : MAIL_ATT_NONE;
+#endif
 
 	if( !chrif_isconnected() )
 		return;
@@ -15013,54 +15416,89 @@ void clif_parse_Mail_getattach(int fd, struct map_session_data *sd)
 	if( i == MAIL_MAX_INBOX )
 		return;
 
-	if( sd->mail.inbox.msg[i].zeny < 1 && (sd->mail.inbox.msg[i].item.nameid < 1 || sd->mail.inbox.msg[i].item.amount < 1) )
-		return;
+	msg = &sd->mail.inbox.msg[i];
 
-	if( sd->mail.inbox.msg[i].zeny + sd->status.zeny > MAX_ZENY ) {
-		clif_Mail_getattachment(fd, 1); //too many zeny
-		return;
+	if( attachment&MAIL_ATT_ZENY && msg->zeny < 1 ){
+		attachment &= ~MAIL_ATT_ZENY;
 	}
+	
+	if( attachment&MAIL_ATT_ITEM ){
+		ARR_FIND(0, MAIL_MAX_ITEM, i, msg->item[i].nameid > 0 || msg->item[i].amount > 0);
 
-	if( sd->mail.inbox.msg[i].item.nameid > 0 ) {
-		struct item_data *data;
-		unsigned int weight;
+		// No items were found
+		if( i == MAIL_MAX_ITEM ){
+			attachment &= ~MAIL_ATT_ITEM;
+		}
+	}
 
-		if ((data = itemdb_exists(sd->mail.inbox.msg[i].item.nameid)) == NULL)
-			return;
+	// Either no attachment requested at all or there are no zeny or items in the mail
+	if( attachment == MAIL_ATT_NONE ){
+		return;
+	}
 
-		switch( pc_checkadditem(sd, data->nameid, sd->mail.inbox.msg[i].item.amount) ) {
-			case CHKADDITEM_NEW:
-				fail = ( pc_inventoryblank(sd) == 0 );
-				break;
-			case CHKADDITEM_OVERAMOUNT:
-				fail = true;
+	if( attachment&MAIL_ATT_ZENY ){
+		if( msg->zeny + sd->status.zeny > MAX_ZENY ){
+			clif_mail_getattachment(sd, msg, 1, MAIL_ATT_ZENY); //too many zeny
+			return;
+		}else{
+			// To make sure another request fails
+			msg->zeny = 0;
 		}
+	}
 
-		if( fail ) {
-			clif_Mail_getattachment(fd, 1);
-			return;
+	if( attachment&MAIL_ATT_ITEM ){
+		int new_ = 0, totalweight = 0;
+
+		for( i = 0; i < MAIL_MAX_ITEM; i++ ){
+			struct item* item = &msg->item[i];
+
+			if( item->nameid > 0 && item->amount > 0 ){
+				struct item_data *data;
+
+				if((data = itemdb_exists(item->nameid)) == NULL)
+					continue;
+
+				switch( pc_checkadditem(sd, item->nameid, item->amount) ){
+					case CHKADDITEM_NEW:
+						new_++;
+						break;
+					case CHKADDITEM_OVERAMOUNT:
+						clif_mail_getattachment(sd, msg, 2, MAIL_ATT_ITEM);
+						return;
+				}
+
+				totalweight += data->weight * item->amount;
+			}
 		}
 
-		weight = data->weight * sd->mail.inbox.msg[i].item.amount;
-		if( sd->weight + weight > sd->max_weight ) {
-			clif_Mail_getattachment(fd, 2);
+		if( ( totalweight + sd->weight ) > sd->max_weight ){
+			clif_mail_getattachment(sd, msg, 2, MAIL_ATT_ITEM);
+			return;
+		}else if( pc_inventoryblank(sd) < new_ ){
+			clif_mail_getattachment(sd, msg, 2, MAIL_ATT_ITEM);
 			return;
 		}
+
+		// To make sure another request fails
+		memset(msg->item, 0, MAIL_MAX_ITEM*sizeof(struct item));
 	}
 
-	sd->mail.inbox.msg[i].zeny = 0;
-	memset(&sd->mail.inbox.msg[i].item, 0, sizeof(struct item));
+	intif_mail_getattach(sd,msg,attachment);
 	clif_Mail_read(sd, mail_id);
-
-	intif_Mail_getattach(sd->status.char_id, mail_id);
 }
 
 
-/// Request to delete a mail (CZ_MAIL_DELETE).
-/// 0243 <mail id>.L
+/// Request to delete a mail.
+/// 0243 <mail id>.L (CZ_MAIL_DELETE)
+/// 09f5 <mail tab>.B <mail id>.Q (CZ_REQ_DELETE_MAIL)
 void clif_parse_Mail_delete(int fd, struct map_session_data *sd){
+#if PACKETVER < 20150513
 	int mail_id = RFIFOL(fd,packet_db[sd->packet_ver][RFIFOW(fd,0)].pos[0]);
-	int i;
+#else
+	int openType = RFIFOB(fd, 2);
+	int mail_id = (int)RFIFOQ(fd, 3);
+#endif
+	int i, j;
 
 	if( !chrif_isconnected() )
 		return;
@@ -15073,12 +15511,24 @@ void clif_parse_Mail_delete(int fd, struct map_session_data *sd){
 	if (i < MAIL_MAX_INBOX) {
 		struct mail_message *msg = &sd->mail.inbox.msg[i];
 
-		if( (msg->item.nameid > 0 && msg->item.amount > 0) || msg->zeny > 0 ) {// can't delete mail without removing attachment first
-			clif_Mail_delete(sd->fd, mail_id, 1);
+		// can't delete mail without removing zeny first
+		if( msg->zeny > 0 ){
+			clif_mail_delete(sd, msg, false);
 			return;
 		}
 
-		intif_Mail_delete(sd->status.char_id, mail_id);
+		// can't delete mail without removing attachment(s) first
+		for( j = 0; j < MAIL_MAX_ITEM; j++ ){
+			if( msg->item[j].nameid > 0 && msg->item[j].amount > 0 ){
+				clif_mail_delete(sd, msg, false);
+				return;
+			}
+		}
+
+		if( intif_Mail_delete(sd->status.char_id, mail_id) && msg->status == MAIL_UNREAD ){
+			sd->mail.inbox.unread--;
+			clif_Mail_new(sd,0,NULL,NULL);
+		}
 	}
 }
 
@@ -15103,12 +15553,17 @@ void clif_parse_Mail_return(int fd, struct map_session_data *sd){
 }
 
 
-/// Request to add an item or Zeny to mail (CZ_MAIL_ADD_ITEM).
-/// 0247 <index>.W <amount>.L
+/// Request to add an item or Zeny to mail.
+/// 0247 <index>.W <amount>.L (CZ_MAIL_ADD_ITEM)
+/// 0a04 <index>.W <amount>.W (CZ_REQ_ADD_ITEM_TO_MAIL)
 void clif_parse_Mail_setattach(int fd, struct map_session_data *sd){
 	struct s_packet_db* info = &packet_db[sd->packet_ver][RFIFOW(fd,0)];
 	int idx = RFIFOW(fd,info->pos[0]);
+#if PACKETVER < 20150513
 	int amount = RFIFOL(fd,info->pos[1]);
+#else
+	int amount = RFIFOW(fd,info->pos[1]);
+#endif
 	unsigned char flag;
 
 	if( !chrif_isconnected() )
@@ -15117,31 +15572,54 @@ void clif_parse_Mail_setattach(int fd, struct map_session_data *sd){
 		return;
 
 	flag = mail_setitem(sd, idx, amount);
-	clif_Mail_setattachment(fd,idx,!flag);
+
+	clif_Mail_setattachment(sd,idx,amount,flag);
 }
 
+/// Remove an item from a mail
+/// 0a07 <result>.B <index>.W <amount>.W <weight>.W
+void clif_mail_removeitem( struct map_session_data* sd, bool success, int index, int amount ){
+	int fd = sd->fd;
+
+	WFIFOHEAD(fd, 9);
+	WFIFOW(fd, 0) = 0xa07;
+	WFIFOB(fd, 2) = success;
+	WFIFOW(fd, 3) = index;
+	WFIFOW(fd, 5) = amount;
+	WFIFOW(fd, 7) = 0; // TODO: which weight? item weight? removed weight? remaining weight?
+	WFIFOSET(fd, 9);
+}
 
-/// Request to reset mail item and/or Zeny (CZ_MAIL_RESET_ITEM).
-/// 0246 <type>.W
+/// Request to reset mail item and/or Zeny
+/// 0246 <type>.W (CZ_MAIL_RESET_ITEM)
 /// type:
 ///     0 = reset all
 ///     1 = remove item
 ///     2 = remove zeny
+/// 0a06 <index>.W <amount>.W (CZ_REQ_REMOVE_ITEM_MAIL)
 void clif_parse_Mail_winopen(int fd, struct map_session_data *sd)
 {
+#if PACKETVER < 20150513
 	int type = RFIFOW(fd,packet_db[sd->packet_ver][RFIFOW(fd,0)].pos[0]);
 
 	if (type == 0 || type == 1)
-		mail_removeitem(sd, 0);
+		mail_removeitem(sd, 0, sd->mail.item[0].index, sd->mail.item[0].amount);
 	if (type == 0 || type == 2)
-		mail_removezeny(sd, 0);
-}
+		mail_removezeny(sd, false);
+#else
+	uint16 index = RFIFOW(fd, 2);
+	uint16 count = RFIFOW(fd, 4);
 
+	mail_removeitem(sd,0,index,count);
+#endif
+}
 
-/// Request to send mail (CZ_MAIL_SEND).
-/// 0248 <packet len>.W <recipient>.24B <title>.40B <body len>.B <body>.?B
-void clif_parse_Mail_send(int fd, struct map_session_data *sd)
-{
+/// Request to send mail
+/// 0248 <packet len>.W <recipient>.24B <title>.40B <body len>.B <body>.?B (CZ_MAIL_SEND)
+/// 09ec <packet len>.W <recipient>.24B <sender>.24B <zeny>.Q <title length>.W <body length>.W <title>.?B <body>.?B (CZ_REQ_WRITE_MAIL)
+/// 0a6e <packet len>.W <recipient>.24B <sender>.24B <zeny>.Q <title length>.W <body length>.W <char id>.L <title>.?B <body>.?B (CZ_REQ_WRITE_MAIL2)
+void clif_parse_Mail_send(int fd, struct map_session_data *sd){
+#if PACKETVER < 20150513
 	struct s_packet_db* info = &packet_db[sd->packet_ver][RFIFOW(fd,0)];
 
 	if( !chrif_isconnected() )
@@ -15153,6 +15631,59 @@ void clif_parse_Mail_send(int fd, struct map_session_data *sd)
 	}
 
 	mail_send(sd, RFIFOCP(fd,info->pos[1]), RFIFOCP(fd,info->pos[2]), RFIFOCP(fd,info->pos[4]), RFIFOB(fd,info->pos[3]));
+#else
+	unsigned short length;
+	static char receiver[NAME_LENGTH];
+	static char sender[NAME_LENGTH];
+	char *title;
+	char *text;
+	uint64 zeny;
+	uint16 titleLength;
+	uint16 textLength;
+
+	length = RFIFOW(fd, 2);
+
+	if( length < 0x3e ){
+		ShowWarning("Too short...\n");
+		clif_Mail_send(sd, WRITE_MAIL_FAILED);
+		return;
+	}
+
+	// Forged request without a begin writing packet?
+	if( !sd->state.mail_writing ){
+		return; // Ignore it
+	}
+
+	safestrncpy(receiver, RFIFOCP(fd, 4), NAME_LENGTH);
+	safestrncpy(sender, RFIFOCP(fd, 28), NAME_LENGTH);
+	zeny = RFIFOQ(fd, 52);
+	titleLength = RFIFOW(fd, 60);
+	textLength = RFIFOW(fd, 62);
+
+	title = (char*)aMalloc(titleLength);
+	text = (char*)aMalloc(textLength);
+
+#if PACKETVER <= 20160330
+	safestrncpy(title, RFIFOCP(fd, 64), titleLength);
+	safestrncpy(text, RFIFOCP(fd, 64 + titleLength), textLength);
+#else
+	// 64 = <char id>.L
+	safestrncpy(title, RFIFOCP(fd, 68), titleLength);
+	safestrncpy(text, RFIFOCP(fd, 68 + titleLength), textLength);
+#endif
+
+	if( zeny > 0 ){
+		if( mail_setitem(sd,0,(uint32)zeny) != MAIL_ATTACH_SUCCESS ){
+			clif_Mail_send(sd,WRITE_MAIL_FAILED);
+			return;
+		}
+	}
+
+	mail_send(sd, receiver, title, text, textLength);
+
+	aFree(title);
+	aFree(text);
+#endif
 }
 
 
@@ -19661,20 +20192,20 @@ void packetdb_readdb(bool reload)
 		0,  0,  0,  8,  8,  0,  0,  0,  0,  0, 23,  17,  0,  0,102,  0,
 		0,  0,  0,  0,  2,  0, -1, -1,  2,  0,  0,  -1,  -1,  -1,  0,  7,
 		0,  0,  0,  0,  0,  18,  22,  3, 11,  0, 11, -1,  0,  3, 11,  0,
-		0, 11, 12, 11,  0,  0,  0,  75,  -1,143,  0,  0,  0,  -1,  -1,  -1,
+		-1, 11, 12, 11,  0,  0,  0,  75,  -1,143,  0,  0,  0,  -1,  -1,  -1,
 	//#0x0A00
 #if PACKETVER >= 20141022
 	  269,  3,  4,  2,  6, 49,  6,  9, 26, 45, 47, 47, 56, -1,  14,  -1,
 #else
 	  269,  0,  0,  2,  6, 48,  6,  9, 26, 45, 47, 47, 56, -1,  14,  0,
 #endif
-		-1,  0,  0, 26,  0,  0,  0,  0,  14,  2, 23,  2, -1,  2,  3,  2,
+		-1,  0,  0, 26, 10,  0,  0,  0,  14,  2, 23,  2, -1,  2,  3,  2,
 	   21,  3,  5,  0, 66,  0,  0,  8,  3,  0,  0,  -1,  0,  -1,  0,  0,
  	  106,  0,  0,  0,  0,  4,  0, 59,  0,  0,  0,  0,  0,  0,  0,  0,
 	//#0x0A40
 		0,  0,  0, 85, -1,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
-		0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
-		0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
+		0, 34,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
+		0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, -1,  0,  0,
 		0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
 	//#0x0A80
 		0,  0,  0,  0, 94,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
@@ -19851,6 +20382,9 @@ void packetdb_readdb(bool reload)
 		{clif_parse_Mail_setattach,"mailsetattach"},
 		{clif_parse_Mail_winopen,"mailwinopen"},
 		{clif_parse_Mail_send,"mailsend"},
+		{clif_parse_Mail_beginwrite,"mailbegin"},
+		{clif_parse_Mail_cancelwrite,"mailcancel"},
+		{clif_parse_Mail_Receiver_Check,"mailreceiver"},
 		// AUCTION SYSTEM
 		{clif_parse_Auction_search,"auctionsearch"},
 		{clif_parse_Auction_buysell,"auctionbuysell"},

+ 16 - 5
src/map/clif.h

@@ -866,14 +866,25 @@ void do_init_clif(void);
 void do_final_clif(void);
 
 // MAIL SYSTEM
+enum mail_send_result{
+	WRITE_MAIL_SUCCESS = 0x0,
+	WRITE_MAIL_FAILED = 0x1,
+	WRITE_MAIL_FAILED_CNT = 0x2,
+	WRITE_MAIL_FAILED_ITEM = 0x3,
+	WRITE_MAIL_FAILED_CHECK_CHARACTER_NAME = 0x4,
+	WRITE_MAIL_FAILED_WHISPEREXREGISTER = 0x5,
+};
+
 void clif_Mail_window(int fd, int flag);
 void clif_Mail_read(struct map_session_data *sd, int mail_id);
-void clif_Mail_delete(int fd, int mail_id, short fail);
+void clif_mail_delete(struct map_session_data* sd, struct mail_message *msg, bool success);
 void clif_Mail_return(int fd, int mail_id, short fail);
-void clif_Mail_send(int fd, bool fail);
-void clif_Mail_new(int fd, int mail_id, const char *sender, const char *title);
-void clif_Mail_refreshinbox(struct map_session_data *sd);
-void clif_Mail_getattachment(int fd, uint8 flag);
+void clif_Mail_send(struct map_session_data* sd, enum mail_send_result result);
+void clif_Mail_new(struct map_session_data* sd, int mail_id, const char *sender, const char *title);
+void clif_Mail_refreshinbox(struct map_session_data *sd,enum mail_inbox_type type,int64 mailID);
+void clif_mail_getattachment(struct map_session_data* sd, struct mail_message *msg, uint8 result, enum mail_attachment_type type);
+void clif_Mail_Receiver_Ack(struct map_session_data* sd, uint32 char_id, short class_, uint32 level, const char* name);
+void clif_mail_removeitem(struct map_session_data* sd, bool success, int index, int amount);
 // AUCTION SYSTEM
 void clif_Auction_openwindow(struct map_session_data *sd);
 void clif_Auction_results(struct map_session_data *sd, short count, short pages, uint8 *buf);

+ 1 - 0
src/map/date.h

@@ -52,6 +52,7 @@ int date_get_year(void);
 enum e_month date_get_month(void);
 int date_get_dayofmonth(void);
 enum e_dayofweek date_get_dayofweek(void);
+int date_get_dayofyear(void);
 int date_get_day(void);
 int date_get_hour(void);
 int date_get_min(void);

+ 90 - 33
src/map/intif.c

@@ -32,7 +32,7 @@ static const int packet_len_table[] = {
 	 0, 0, 0, 0,  0, 0, 0, 0, -1,11, 0, 0,  0, 0,  0, 0, //0x3810
 	39,-1,15,15, 15+NAME_LENGTH,19, 7,-1,  0, 0, 0, 0,  0, 0,  0, 0, //0x3820
 	10,-1,15, 0, 79,19, 7,-1,  0,-1,-1,-1, 14,67,186,-1, //0x3830
-	-1, 0, 0,14,  0, 0, 0, 0, -1,74,-1,11, 11,-1,  0, 0, //0x3840
+	-1, 0, 0,14,  0, 0, 0, 0, -1,75,-1,11, 11,-1, 38, 0, //0x3840
 	-1,-1, 7, 7,  7,11, 8,-1,  0, 0, 0, 0,  0, 0,  0, 0, //0x3850  Auctions [Zephyrus] itembound[Akinari]
 	-1, 7, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0,  0, 0, //0x3860  Quests [Kevin] [Inkfish]
 	-1, 3, 3, 0,  0, 0, 0, 0,  0, 0, 0, 0, -1, 3,  3, 0, //0x3870  Mercenaries [Zephyrus] / Elemental [pakpil]
@@ -2087,16 +2087,17 @@ int intif_quest_save(struct map_session_data *sd)
  * @param flag 0 Update Inbox | 1 OpenMail
  * @return 0=errur, 1=msg_sent
  */
-int intif_Mail_requestinbox(uint32 char_id, unsigned char flag)
+int intif_Mail_requestinbox(uint32 char_id, unsigned char flag, enum mail_inbox_type type)
 {
 	if (CheckForCharServer())
 		return 0;
 
-	WFIFOHEAD(inter_fd,7);
+	WFIFOHEAD(inter_fd,8);
 	WFIFOW(inter_fd,0) = 0x3048;
 	WFIFOL(inter_fd,2) = char_id;
 	WFIFOB(inter_fd,6) = flag;
-	WFIFOSET(inter_fd,7);
+	WFIFOB(inter_fd,7) = type;
+	WFIFOSET(inter_fd,8);
 
 	return 1;
 }
@@ -2120,24 +2121,29 @@ int intif_parse_Mail_inboxreceived(int fd)
 		return 0;
 	}
 
-	if (RFIFOW(fd,2) - 9 != sizeof(struct mail_data))
+	if (RFIFOW(fd,2) - 10 != sizeof(struct mail_data))
 	{
-		ShowError("intif_parse_Mail_inboxreceived: data size error %d %d\n", RFIFOW(fd,2) - 9, sizeof(struct mail_data));
+		ShowError("intif_parse_Mail_inboxreceived: data size error %d %d\n", RFIFOW(fd,2) - 10, sizeof(struct mail_data));
 		return 0;
 	}
 
 	//FIXME: this operation is not safe [ultramage]
-	memcpy(&sd->mail.inbox, RFIFOP(fd,9), sizeof(struct mail_data));
+	memcpy(&sd->mail.inbox, RFIFOP(fd,10), sizeof(struct mail_data));
 	sd->mail.changed = false; // cache is now in sync
 
-	if (flag)
-		clif_Mail_refreshinbox(sd);
-	else if( battle_config.mail_show_status && ( battle_config.mail_show_status == 1 || sd->mail.inbox.unread ) )
+	if (flag){
+#if PACKETVER >= 20150513
+		// Refresh top right icon
+		clif_Mail_new(sd, 0, NULL, NULL);
+#endif
+		clif_Mail_refreshinbox(sd,RFIFOB(fd,9),0);
+	}else if( battle_config.mail_show_status && ( battle_config.mail_show_status == 1 || sd->mail.inbox.unread ) )
 	{
 		char output[128];
 		sprintf(output, msg_txt(sd,510), sd->mail.inbox.unchecked, sd->mail.inbox.unread + sd->mail.inbox.unchecked);
 		clif_messagecolor(&sd->bl, color_table[COLOR_LIGHT_GREEN], output, false, SELF);
 	}
+
 	return 1;
 }
 
@@ -2165,18 +2171,18 @@ int intif_Mail_read(int mail_id)
  * @param mail_id : Mail identification
  * @return 0=error, 1=msg sent
  */
-int intif_Mail_getattach(uint32 char_id, int mail_id)
-{
+bool intif_mail_getattach( struct map_session_data* sd, struct mail_message *msg, enum mail_attachment_type type){
 	if (CheckForCharServer())
-		return 0;
+		return false;
 
-	WFIFOHEAD(inter_fd,10);
+	WFIFOHEAD(inter_fd,11);
 	WFIFOW(inter_fd,0) = 0x304a;
-	WFIFOL(inter_fd,2) = char_id;
-	WFIFOL(inter_fd,6) = mail_id;
-	WFIFOSET(inter_fd, 10);
+	WFIFOL(inter_fd,2) = sd->status.char_id;
+	WFIFOL(inter_fd,6) = msg->id;
+	WFIFOB(inter_fd,10) = (uint8)type;
+	WFIFOSET(inter_fd, 11);
 
-	return 1;
+	return true;
 }
 
 /**
@@ -2187,8 +2193,14 @@ int intif_Mail_getattach(uint32 char_id, int mail_id)
 int intif_parse_Mail_getattach(int fd)
 {
 	struct map_session_data *sd;
-	struct item item;
-	int zeny = RFIFOL(fd,8);
+	struct item item[MAIL_MAX_ITEM];
+	int i, mail_id, zeny;
+
+	if (RFIFOW(fd, 2) - 16 != sizeof(struct item)*MAIL_MAX_ITEM)
+	{
+		ShowError("intif_parse_Mail_getattach: data size error %d %d\n", RFIFOW(fd, 2) - 16, sizeof(struct item));
+		return 0;
+	}
 
 	sd = map_charid2sd( RFIFOL(fd,4) );
 
@@ -2198,15 +2210,17 @@ int intif_parse_Mail_getattach(int fd)
 		return 0;
 	}
 
-	if (RFIFOW(fd,2) - 12 != sizeof(struct item))
-	{
-		ShowError("intif_parse_Mail_getattach: data size error %d %d\n", RFIFOW(fd,2) - 16, sizeof(struct item));
+	mail_id = RFIFOL(fd, 8);
+
+	ARR_FIND(0, MAIL_MAX_INBOX, i, sd->mail.inbox.msg[i].id == mail_id);
+	if (i == MAIL_MAX_INBOX)
 		return 0;
-	}
 
-	memcpy(&item, RFIFOP(fd,12), sizeof(struct item));
+	zeny = RFIFOL(fd, 12);
 
-	mail_getattachment(sd, zeny, &item);
+	memcpy(item, RFIFOP(fd,16), sizeof(struct item)*MAIL_MAX_ITEM);
+
+	mail_getattachment(sd, &sd->mail.inbox.msg[i], zeny, item);
 	return 1;
 }
 
@@ -2240,6 +2254,7 @@ int intif_parse_Mail_delete(int fd)
 	uint32 char_id = RFIFOL(fd,2);
 	int mail_id = RFIFOL(fd,6);
 	bool failed = RFIFOB(fd,10);
+	enum mail_inbox_type type;
 
 	struct map_session_data *sd = map_charid2sd(char_id);
 	if (sd == NULL)
@@ -2254,15 +2269,16 @@ int intif_parse_Mail_delete(int fd)
 		ARR_FIND(0, MAIL_MAX_INBOX, i, sd->mail.inbox.msg[i].id == mail_id);
 		if( i < MAIL_MAX_INBOX )
 		{
+			clif_mail_delete(sd, &sd->mail.inbox.msg[i], !failed);
+			type = sd->mail.inbox.msg[i].type;
 			memset(&sd->mail.inbox.msg[i], 0, sizeof(struct mail_message));
 			sd->mail.inbox.amount--;
 		}
 
-		if( sd->mail.inbox.full )
-			intif_Mail_requestinbox(sd->status.char_id, 1); // Free space is available for new mails
+		if( sd->mail.inbox.full || sd->mail.inbox.unchecked > 0 )
+			intif_Mail_requestinbox(sd->status.char_id, 1, type); // Free space is available for new mails
 	}
 
-	clif_Mail_delete(sd->fd, mail_id, failed);
 	return 1;
 }
 
@@ -2300,6 +2316,7 @@ int intif_parse_Mail_return(int fd)
 	struct map_session_data *sd = map_charid2sd(RFIFOL(fd,2));
 	int mail_id = RFIFOL(fd,6);
 	short fail = RFIFOB(fd,10);
+	enum mail_inbox_type type;
 
 	if( sd == NULL )
 	{
@@ -2313,12 +2330,13 @@ int intif_parse_Mail_return(int fd)
 		ARR_FIND(0, MAIL_MAX_INBOX, i, sd->mail.inbox.msg[i].id == mail_id);
 		if( i < MAIL_MAX_INBOX )
 		{
+			type = sd->mail.inbox.msg[i].type;
 			memset(&sd->mail.inbox.msg[i], 0, sizeof(struct mail_message));
 			sd->mail.inbox.amount--;
 		}
 
 		if( sd->mail.inbox.full )
-			intif_Mail_requestinbox(sd->status.char_id, 1); // Free space is available for new mails
+			intif_Mail_requestinbox(sd->status.char_id, 1, type); // Free space is available for new mails
 	}
 
 	clif_Mail_return(sd->fd, mail_id, fail);
@@ -2379,7 +2397,7 @@ static void intif_parse_Mail_send(int fd)
 			mail_deliveryfail(sd, &msg);
 		else
 		{
-			clif_Mail_send(sd->fd, false);
+			clif_Mail_send(sd, WRITE_MAIL_SUCCESS);
 			if( save_settings&CHARSAVE_MAIL )
 				chrif_save(sd, CSAVE_INVENTORY);
 		}
@@ -2399,9 +2417,47 @@ static void intif_parse_Mail_new(int fd)
 
 	if( sd == NULL )
 		return;
-
 	sd->mail.changed = true;
-	clif_Mail_new(sd->fd, mail_id, sender_name, title);
+	sd->mail.inbox.unread++;
+	clif_Mail_new(sd, mail_id, sender_name, title);
+#if PACKETVER >= 20150513
+	// Make sure the window gets refreshed when its open
+	intif_Mail_requestinbox(sd->status.char_id, 1, RFIFOB(fd,74));
+#endif
+}
+
+static void intif_parse_Mail_receiver( int fd ){
+	struct map_session_data *sd;
+
+	sd = map_charid2sd( RFIFOL( fd, 2 ) );
+
+	// Only f the player is online
+	if( sd ){
+		clif_Mail_Receiver_Ack( sd, RFIFOL( fd, 6 ), RFIFOW( fd, 10 ), RFIFOW( fd, 12 ), RFIFOCP( fd, 14 ) );
+	}
+}
+
+bool intif_mail_checkreceiver( struct map_session_data* sd, char* name ){
+	struct map_session_data *tsd;
+
+	tsd = map_nick2sd( name, false );
+
+	// If the target player is online on this map-server
+	if( tsd != NULL ){
+		clif_Mail_Receiver_Ack( sd, tsd->status.char_id, tsd->status.class_, tsd->status.base_level, name );
+		return true;
+	}
+
+	if( CheckForCharServer() )
+		return false;
+
+	WFIFOHEAD(inter_fd, 6 + NAME_LENGTH);
+	WFIFOW(inter_fd, 0) = 0x304e;
+	WFIFOL(inter_fd, 2) = sd->status.char_id;
+	safestrncpy(WFIFOCP(inter_fd, 6), name, NAME_LENGTH);
+	WFIFOSET(inter_fd, 6 + NAME_LENGTH);
+
+	return true;
 }
 
 /*==========================================
@@ -3525,6 +3581,7 @@ int intif_parse(int fd)
 	case 0x384b:	intif_parse_Mail_delete(fd); break;
 	case 0x384c:	intif_parse_Mail_return(fd); break;
 	case 0x384d:	intif_parse_Mail_send(fd); break;
+	case 0x384e:	intif_parse_Mail_receiver(fd); break;
 
 	// Auction System
 	case 0x3850:	intif_parse_Auction_results(fd); break;

+ 3 - 2
src/map/intif.h

@@ -89,12 +89,13 @@ int intif_mercenary_delete(int merc_id);
 int intif_mercenary_save(struct s_mercenary *merc);
 
 // MAIL SYSTEM
-int intif_Mail_requestinbox(uint32 char_id, unsigned char flag);
+int intif_Mail_requestinbox(uint32 char_id, unsigned char flag, enum mail_inbox_type type);
 int intif_Mail_read(int mail_id);
-int intif_Mail_getattach(uint32 char_id, int mail_id);
+bool intif_mail_getattach( struct map_session_data* sd, struct mail_message *msg, enum mail_attachment_type type );
 int intif_Mail_delete(uint32 char_id, int mail_id);
 int intif_Mail_return(uint32 char_id, int mail_id);
 int intif_Mail_send(uint32 account_id, struct mail_message *msg);
+bool intif_mail_checkreceiver(struct map_session_data* sd, char* name);
 // AUCTION SYSTEM
 int intif_Auction_requestlist(uint32 char_id, short type, int price, const char* searchtext, short page);
 int intif_Auction_register(struct auction_data *auction);

+ 240 - 75
src/map/mail.c

@@ -11,48 +11,98 @@
 #include "clif.h"
 #include "pc.h"
 #include "intif.h"
+#include "date.h" // date_get_dayofyear
 
 void mail_clear(struct map_session_data *sd)
 {
-	sd->mail.nameid = 0;
-	sd->mail.index = 0;
-	sd->mail.amount = 0;
+	int i;
+
+	for( i = 0; i < MAIL_MAX_ITEM; i++ ){
+		sd->mail.item[i].nameid = 0;
+		sd->mail.item[i].index = 0;
+		sd->mail.item[i].amount = 0;
+	}
 	sd->mail.zeny = 0;
 
 	return;
 }
 
-int mail_removeitem(struct map_session_data *sd, short flag)
+int mail_removeitem(struct map_session_data *sd, short flag, int idx, int amount)
 {
+	int i;
+
 	nullpo_ret(sd);
 
-	if( sd->mail.amount )
-	{
-		if (flag) // Item send
-			pc_delitem(sd, sd->mail.index, sd->mail.amount, 1, 0, LOG_TYPE_MAIL);
-		else
-			clif_additem(sd, sd->mail.index, sd->mail.amount, 0);
+	idx -= 2;
+
+	if( idx < 0 || idx >= MAX_INVENTORY )
+			return false;
+	if( amount <= 0 || amount > sd->inventory.u.items_inventory[idx].amount )
+			return false;
+
+	ARR_FIND(0, MAIL_MAX_ITEM, i, sd->mail.item[i].index == idx && sd->mail.item[i].nameid > 0);
+
+	if( i == MAIL_MAX_ITEM ){
+		return false;
+	}
+
+	if( flag ){
+		if( battle_config.mail_attachment_price > 0 ){
+			if( pc_payzeny( sd, battle_config.mail_attachment_price, LOG_TYPE_MAIL, NULL ) ){
+				return false;
+			}
+		}
+
+#if PACKETVER < 20150513
+		// With client update packet
+		pc_delitem(sd, idx, amount, 1, 0, LOG_TYPE_MAIL);
+#else
+		// RODEX refreshes the client inventory from the ACK packet
+		pc_delitem(sd, idx, amount, 0, 0, LOG_TYPE_MAIL);
+#endif
+	}else{
+		for( ; i < MAIL_MAX_ITEM-1; i++ ){
+			if (sd->mail.item[i + 1].nameid == 0)
+				break;
+			sd->mail.item[i].index = sd->mail.item[i+1].index;
+			sd->mail.item[i].nameid = sd->mail.item[i+1].nameid;
+			sd->mail.item[i].amount = sd->mail.item[i+1].amount;
+		}
+
+		for( ; i < MAIL_MAX_ITEM; i++ ){
+			sd->mail.item[i].index = 0;
+			sd->mail.item[i].nameid = 0;
+			sd->mail.item[i].amount = 0;
+		}
+
+#if PACKETVER < 20150513
+		clif_additem(sd, idx, amount, 0);
+#else
+		clif_mail_removeitem(sd, true, idx + 2, amount);
+#endif
 	}
 
-	sd->mail.nameid = 0;
-	sd->mail.index = 0;
-	sd->mail.amount = 0;
 	return 1;
 }
 
-int mail_removezeny(struct map_session_data *sd, short flag)
-{
-	nullpo_ret(sd);
-
-	if (flag && sd->mail.zeny > 0)
-	{  //Zeny send
-		pc_payzeny(sd,sd->mail.zeny,LOG_TYPE_MAIL, NULL);
+bool mail_removezeny( struct map_session_data *sd, bool flag ){
+	nullpo_retr( false, sd );
+
+	if( sd->mail.zeny > 0 ){
+		//Zeny send
+		if( flag ){
+			if( pc_payzeny( sd, sd->mail.zeny + sd->mail.zeny * battle_config.mail_zeny_fee / 100, LOG_TYPE_MAIL, NULL ) ){
+				return false;
+			}
+		}else{
+			// Update is called by pc_payzeny, so only call it in the else condition
+			clif_updatestatus(sd, SP_ZENY);
+		}
 	}
-	if (sd->mail.zeny > 0)
-		clif_updatestatus(sd, SP_ZENY);
+
 	sd->mail.zeny = 0;
 
-	return 1;
+	return true;
 }
 
 /**
@@ -60,92 +110,175 @@ int mail_removezeny(struct map_session_data *sd, short flag)
 * @param sd : player attaching the content
 * @param idx 0 - Zeny; >= 2 - Inventory item
 * @param amount : amout of zeny or number of item
-* @return True if item/zeny can be set, False if failed
+* @return see enum mail_attach_result in mail.h
 */
-bool mail_setitem(struct map_session_data *sd, short idx, uint32 amount) {
-
+enum mail_attach_result mail_setitem(struct map_session_data *sd, short idx, uint32 amount) {
 	if( pc_istrading(sd) )
-		return false;
+		return MAIL_ATTACH_ERROR;
 
 	if( idx == 0 ) { // Zeny Transfer
 		if( !pc_can_give_items(sd) )
-			return false;
+			return MAIL_ATTACH_UNTRADEABLE;
 
+#if PACKETVER < 20150513
 		if( amount > sd->status.zeny )
-			amount = sd->status.zeny;
+			amount = sd->status.zeny; // TODO: confirm this behavior for old mail system
+#else
+		if( ( amount + battle_config.mail_zeny_fee / 100 * amount ) > sd->status.zeny )
+			return MAIL_ATTACH_ERROR;
+#endif
 
 		sd->mail.zeny = amount;
 		// clif_updatestatus(sd, SP_ZENY);
-		return true;
+		return MAIL_ATTACH_SUCCESS;
 	} else { // Item Transfer
+		int i, j, total = 0;
+
 		idx -= 2;
-		mail_removeitem(sd, 0);
 
 		if( idx < 0 || idx >= MAX_INVENTORY )
-			return false;
+			return MAIL_ATTACH_ERROR;
+
+#if PACKETVER < 20150513
+		i = 0;
+		// Remove existing item
+		mail_removeitem(sd, 0, sd->mail.item[i].index + 2, sd->mail.item[i].amount);
+#else
+		ARR_FIND(0, MAIL_MAX_ITEM, i, sd->mail.item[i].index == idx && sd->mail.item[i].nameid > 0 );
+		
+		// The same item had already been added to the mail
+		if( i < MAIL_MAX_ITEM ){
+			// Check if it is stackable
+			if( !itemdb_isstackable(sd->mail.item[i].nameid) ){
+				return MAIL_ATTACH_ERROR;
+			}
+
+			// Check if it exceeds the total amount
+			if( ( amount + sd->mail.item[i].amount ) > sd->inventory.u.items_inventory[idx].amount ){
+				return MAIL_ATTACH_ERROR;
+			}
+
+			// Check if it exceeds the total weight
+			if( battle_config.mail_attachment_weight ){
+				for( j = 0; j < i; j++ ){
+					total += sd->mail.item[j].amount * ( sd->inventory_data[sd->mail.item[j].index]->weight / 10 );
+				}
+
+				total += amount * sd->inventory_data[idx]->weight / 10;
+
+				if( total > battle_config.mail_attachment_weight ){
+					return MAIL_ATTACH_WEIGHT;
+				}
+			}
+
+			sd->mail.item[i].amount += amount;
+
+			return MAIL_ATTACH_SUCCESS;
+		}else{
+			ARR_FIND(0, MAIL_MAX_ITEM, i, sd->mail.item[i].nameid == 0);
+
+			if( i == MAIL_MAX_ITEM ){
+				return MAIL_ATTACH_SPACE;
+			}
+
+			// Check if it exceeds the total weight
+			if( battle_config.mail_attachment_weight ){
+				for( j = 0; j < i; j++ ){
+					total += sd->mail.item[j].amount * ( sd->inventory_data[sd->mail.item[j].index]->weight / 10 );
+				}
+
+				total += amount * sd->inventory_data[idx]->weight / 10;
+
+				if( total > battle_config.mail_attachment_weight ){
+					return MAIL_ATTACH_WEIGHT;
+				}
+			}
+		}
+#endif
+
 		if( amount > sd->inventory.u.items_inventory[idx].amount )
-			return false;
+			return MAIL_ATTACH_ERROR;
 		if( !pc_can_give_items(sd) || sd->inventory.u.items_inventory[idx].expire_time
 			|| !itemdb_available(sd->inventory.u.items_inventory[idx].nameid)
 			|| !itemdb_canmail(&sd->inventory.u.items_inventory[idx],pc_get_group_level(sd))
 			|| (sd->inventory.u.items_inventory[idx].bound && !pc_can_give_bounded_items(sd)) )
-			return false;
+			return MAIL_ATTACH_UNTRADEABLE;
 
-		sd->mail.index = idx;
-		sd->mail.nameid = sd->inventory.u.items_inventory[idx].nameid;
-		sd->mail.amount = amount;
-		return true;
+		sd->mail.item[i].index = idx;
+		sd->mail.item[i].nameid = sd->inventory.u.items_inventory[idx].nameid;
+		sd->mail.item[i].amount = amount;
+		return MAIL_ATTACH_SUCCESS;
 	}
 }
 
 bool mail_setattachment(struct map_session_data *sd, struct mail_message *msg)
 {
-	int n;
+	int i, amount;
 
 	nullpo_retr(false,sd);
 	nullpo_retr(false,msg);
 
-	if( sd->mail.zeny < 0 || sd->mail.zeny > sd->status.zeny )
-		return false;
+	for( i = 0, amount = 0; i < MAIL_MAX_ITEM; i++ ){
+		int index = sd->mail.item[i].index;
 
-	n = sd->mail.index;
-	if( sd->mail.amount )
-	{
-		if( sd->inventory.u.items_inventory[n].nameid != sd->mail.nameid )
+		if( sd->mail.item[i].nameid == 0 || sd->mail.item[i].amount == 0 ){
+			memset(&msg->item[i], 0x00, sizeof(struct item));
+			continue;
+		}
+
+		amount++;
+
+		if( sd->inventory.u.items_inventory[index].nameid != sd->mail.item[i].nameid )
 			return false;
 
-		if( sd->inventory.u.items_inventory[n].amount < sd->mail.amount )
+		if( sd->inventory.u.items_inventory[index].amount < sd->mail.item[i].amount )
 			return false;
 
-		if( sd->weight > sd->max_weight )
+		if( sd->weight > sd->max_weight ) // TODO: Why check something weird like this here?
 			return false;
 
-		memcpy(&msg->item, &sd->inventory.u.items_inventory[n], sizeof(struct item));
-		msg->item.amount = sd->mail.amount;
+		memcpy(&msg->item[i], &sd->inventory.u.items_inventory[index], sizeof(struct item));
+		msg->item[i].amount = sd->mail.item[i].amount;
 	}
-	else
-		memset(&msg->item, 0x00, sizeof(struct item));
+
+	if( sd->mail.zeny < 0 || ( sd->mail.zeny + sd->mail.zeny * battle_config.mail_zeny_fee / 100 + amount * battle_config.mail_attachment_price ) > sd->status.zeny )
+		return false;
 
 	msg->zeny = sd->mail.zeny;
 
 	// Removes the attachment from sender
-	mail_removeitem(sd,1);
-	mail_removezeny(sd,1);
+	for( i = 0; i < MAIL_MAX_ITEM; i++ ){
+		if( sd->mail.item[i].nameid == 0 || sd->mail.item[i].amount == 0 ){
+			// Exit the loop on the first empty entry
+			break;
+		}
+
+		mail_removeitem(sd,1,sd->mail.item[i].index + 2,sd->mail.item[i].amount);
+	}
+	mail_removezeny(sd,true);
 
 	return true;
 }
 
-void mail_getattachment(struct map_session_data* sd, int zeny, struct item* item)
-{
-	if( item->nameid > 0 && item->amount > 0 )
-	{
-		pc_additem(sd, item, item->amount, LOG_TYPE_MAIL);
-		clif_Mail_getattachment(sd->fd, 0);
+void mail_getattachment(struct map_session_data* sd, struct mail_message* msg, int zeny, struct item* item){
+	int i;
+	bool item_received = false;
+
+	for( i = 0; i < MAIL_MAX_ITEM; i++ ){
+		if( item->nameid > 0 && item->amount > 0 ){
+			pc_additem(sd, &item[i], item[i].amount, LOG_TYPE_MAIL);
+			item_received = true;
+		}	
+	}
+
+	if( item_received ){
+		clif_mail_getattachment( sd, msg, 0, MAIL_ATT_ITEM );
 	}
 
-	if( zeny > 0 )
-	{  //Zeny receive
+	// Zeny receive
+	if( zeny > 0 ){
 		pc_getzeny(sd, zeny,LOG_TYPE_MAIL, NULL);
+		clif_mail_getattachment( sd, msg, 0, MAIL_ATT_ZENY );
 	}
 }
 
@@ -161,33 +294,37 @@ int mail_openmail(struct map_session_data *sd)
 	return 1;
 }
 
-void mail_deliveryfail(struct map_session_data *sd, struct mail_message *msg)
-{
+void mail_deliveryfail(struct map_session_data *sd, struct mail_message *msg){
+	int i, zeny = 0;
+
 	nullpo_retv(sd);
 	nullpo_retv(msg);
 
-	if( msg->item.amount > 0 )
-	{
-		// Item receive (due to failure)
-		pc_additem(sd, &msg->item, msg->item.amount, LOG_TYPE_MAIL);
+	for( i = 0; i < MAIL_MAX_ITEM; i++ ){
+		if( msg->item[i].amount > 0 ){
+			// Item receive (due to failure)
+			pc_additem(sd, &msg->item[i], msg->item[i].amount, LOG_TYPE_MAIL);
+			zeny += battle_config.mail_attachment_price;
+		}
 	}
 
-	if( msg->zeny > 0 )
-	{
-		pc_getzeny(sd,msg->zeny,LOG_TYPE_MAIL, NULL); //Zeny receive (due to failure)
+	if( msg->zeny > 0 ){
+		pc_getzeny(sd,msg->zeny + msg->zeny*battle_config.mail_zeny_fee/100 + zeny,LOG_TYPE_MAIL, NULL); //Zeny receive (due to failure)
 	}
 
-	clif_Mail_send(sd->fd, true);
+	clif_Mail_send(sd, WRITE_MAIL_FAILED);
 }
 
 // This function only check if the mail operations are valid
 bool mail_invalid_operation(struct map_session_data *sd)
 {
+#if PACKETVER < 20150513
 	if( !map[sd->bl.m].flag.town && !pc_can_use_command(sd, "mail", COMMAND_ATCOMMAND) )
 	{
 		ShowWarning("clif_parse_Mail: char '%s' trying to do invalid mail operations.\n", sd->status.name);
 		return true;
 	}
+#endif
 
 	return false;
 }
@@ -210,17 +347,33 @@ void mail_send(struct map_session_data *sd, const char *dest_name, const char *t
 
 	if( DIFF_TICK(sd->cansendmail_tick, gettick()) > 0 ) {
 		clif_displaymessage(sd->fd,msg_txt(sd,675)); //"Cannot send mails too fast!!."
-		clif_Mail_send(sd->fd, true); // fail
+		clif_Mail_send(sd, WRITE_MAIL_FAILED); // fail
 		return;
 	}
 
+	if( battle_config.mail_daily_count ){
+		mail_refresh_remaining_amount(sd);
+
+		// After calling mail_refresh_remaining_amount the status should always be there
+		if( sd->sc.data[SC_DAILYSENDMAILCNT] == NULL || sd->sc.data[SC_DAILYSENDMAILCNT]->val2 >= battle_config.mail_daily_count ){
+			clif_Mail_send(sd, WRITE_MAIL_FAILED_CNT);
+			return;
+		}else{
+			sc_start2( &sd->bl, &sd->bl, SC_DAILYSENDMAILCNT, 100, date_get_dayofyear(), sd->sc.data[SC_DAILYSENDMAILCNT]->val2 + 1, -1 );
+		}
+	}
+
 	if( body_len > MAIL_BODY_LENGTH )
 		body_len = MAIL_BODY_LENGTH;
 
 	if( !mail_setattachment(sd, &msg) ) { // Invalid Append condition
-		clif_Mail_send(sd->fd, true); // fail
-		mail_removeitem(sd,0);
-		mail_removezeny(sd,0);
+		int i;
+
+		clif_Mail_send(sd, WRITE_MAIL_FAILED); // fail
+		for( i = 0; i < MAIL_MAX_ITEM; i++ ){
+			mail_removeitem(sd,0,sd->mail.item[i].index + 2, sd->mail.item[i].amount);
+		}
+		mail_removezeny(sd,false);
 		return;
 	}
 
@@ -230,6 +383,7 @@ void mail_send(struct map_session_data *sd, const char *dest_name, const char *t
 	safestrncpy(msg.send_name, sd->status.name, NAME_LENGTH);
 	safestrncpy(msg.dest_name, (char*)dest_name, NAME_LENGTH);
 	safestrncpy(msg.title, (char*)title, MAIL_TITLE_LENGTH);
+	msg.type = MAIL_INBOX_NORMAL;
 
 	if (msg.title[0] == '\0') {
 		return; // Message has no length and somehow client verification was skipped.
@@ -246,3 +400,14 @@ void mail_send(struct map_session_data *sd, const char *dest_name, const char *t
 
 	sd->cansendmail_tick = gettick() + battle_config.mail_delay; // Flood Protection
 }
+
+void mail_refresh_remaining_amount( struct map_session_data* sd ){
+	int doy = date_get_dayofyear();
+
+	nullpo_retv(sd);
+
+	// If it was not yet started or it was started on another day
+	if( sd->sc.data[SC_DAILYSENDMAILCNT] == NULL || sd->sc.data[SC_DAILYSENDMAILCNT]->val1 != doy ){
+		sc_start2( &sd->bl, &sd->bl, SC_DAILYSENDMAILCNT, 100, doy, 0, -1 );
+	}
+}

+ 20 - 4
src/map/mail.h

@@ -6,15 +6,31 @@
 
 #include "../common/mmo.h"
 
+enum mail_attach_result {
+	MAIL_ATTACH_SUCCESS = 0,
+#if PACKETVER >= 20150513
+	MAIL_ATTACH_WEIGHT = 1,
+	MAIL_ATTACH_ERROR = 2,
+	MAIL_ATTACH_SPACE = 3,
+	MAIL_ATTACH_UNTRADEABLE = 4
+#else
+	MAIL_ATTACH_WEIGHT = 1,
+	MAIL_ATTACH_ERROR = 1,
+	MAIL_ATTACH_SPACE = 1,
+	MAIL_ATTACH_UNTRADEABLE = 1
+#endif
+};
+
 void mail_clear(struct map_session_data *sd);
-int mail_removeitem(struct map_session_data *sd, short flag);
-int mail_removezeny(struct map_session_data *sd, short flag);
-bool mail_setitem(struct map_session_data *sd, short idx, uint32 amount);
+int mail_removeitem(struct map_session_data *sd, short flag, int idx, int amount);
+bool mail_removezeny(struct map_session_data *sd, bool flag);
+enum mail_attach_result mail_setitem(struct map_session_data *sd, short idx, uint32 amount);
 bool mail_setattachment(struct map_session_data *sd, struct mail_message *msg);
-void mail_getattachment(struct map_session_data* sd, int zeny, struct item* item);
+void mail_getattachment(struct map_session_data* sd, struct mail_message* msg, int zeny, struct item* item);
 int mail_openmail(struct map_session_data *sd);
 void mail_deliveryfail(struct map_session_data *sd, struct mail_message *msg);
 bool mail_invalid_operation(struct map_session_data *sd);
 void mail_send(struct map_session_data *sd, const char *dest_name, const char *title, const char *body_msg, int body_len);
+void mail_refresh_remaining_amount( struct map_session_data* sd );
 
 #endif /* _MAIL_H_ */

+ 1 - 1
src/map/pc.c

@@ -1449,7 +1449,7 @@ void pc_reg_received(struct map_session_data *sd)
 	sd->vip.enabled = 0;
 	chrif_req_login_operation(sd->status.account_id, sd->status.name, CHRIF_OP_LOGIN_VIP, 0, 1|8, 0);  // request VIP information
 #endif
-	intif_Mail_requestinbox(sd->status.char_id, 0); // MAIL SYSTEM - Request Mail Inbox
+	intif_Mail_requestinbox(sd->status.char_id, 0, MAIL_INBOX_NORMAL); // MAIL SYSTEM - Request Mail Inbox
 	intif_request_questlog(sd);
 
 	if (sd->state.connect_new == 0 && sd->fd) { //Character already loaded map! Gotta trigger LoadEndAck manually.

+ 6 - 2
src/map/pc.h

@@ -262,6 +262,7 @@ struct map_session_data {
 		unsigned int workinprogress : 2; // See clif.h::e_workinprogress
 		bool pc_loaded; // Ensure inventory data and status data is loaded before we calculate player stats
 		bool keepshop; // Whether shop data should be removed when the player disconnects
+		bool mail_writing; // Whether the player is currently writing a mail in RODEX or not
 	} state;
 	struct {
 		unsigned char no_weapon_damage, no_magic_damage, no_misc_damage;
@@ -583,8 +584,11 @@ struct map_session_data {
 
 	// Mail System [Zephyrus]
 	struct s_mail {
-		unsigned short nameid;
-		int index, amount, zeny;
+		struct {
+			unsigned short nameid;
+			int index, amount;
+		} item[MAIL_MAX_ITEM];
+		int zeny;
 		struct mail_data inbox;
 		bool changed; // if true, should sync with charserver on next mailbox request
 	} mail;

+ 20 - 0
src/map/script.c

@@ -23081,6 +23081,25 @@ BUILDIN_FUNC(channel_delete) {
 	return SCRIPT_CMD_SUCCESS;
 }
 
+BUILDIN_FUNC(unloadnpc) {
+	const char *name;
+	struct npc_data* nd;
+
+	name = script_getstr(st, 2);
+	nd = npc_name2id(name);
+
+	if( nd == NULL ){
+		ShowError( "buildin_unloadnpc: npc '%s' was not found.\n", name );
+		return SCRIPT_CMD_FAILURE;
+	}
+
+	npc_unload_duplicates(nd);
+	npc_unload(nd, true);
+	npc_read_event_script();
+
+	return SCRIPT_CMD_SUCCESS;
+}
+
 #include "../custom/script.inc"
 
 // declarations that were supposed to be exported from npc_chat.c
@@ -23682,6 +23701,7 @@ struct script_function buildin_func[] = {
 	BUILDIN_DEF(needed_status_point,"ii?"),
 	BUILDIN_DEF(jobcanentermap,"s?"),
 	BUILDIN_DEF(openstorage2,"ii?"),
+	BUILDIN_DEF(unloadnpc, "s"),
 
 	// WoE TE
 	BUILDIN_DEF(agitstart3,""),

+ 1 - 0
src/map/script_constants.h

@@ -1466,6 +1466,7 @@
 	export_constant(SC_ARMOR_ELEMENT_EARTH);
 	export_constant(SC_ARMOR_ELEMENT_FIRE);
 	export_constant(SC_ARMOR_ELEMENT_WIND);
+	export_constant(SC_DAILYSENDMAILCNT);
 #ifdef RENEWAL
 	export_constant(SC_EXTREMITYFIST2);
 #endif

+ 3 - 0
src/map/skill.c

@@ -1886,6 +1886,7 @@ int skill_additional_effect(struct block_list* src, struct block_list *bl, uint1
 					case SC_SPRITEMABLE:		case SC_BITESCAR:
 					case SC_CLAN_INFO:		case SC_SWORDCLAN:		case SC_ARCWANDCLAN:
 					case SC_GOLDENMACECLAN:	case SC_CROSSBOWCLAN:
+					case SC_DAILYSENDMAILCNT:
 						continue;
 					case SC_WHISTLE:		case SC_ASSNCROS:		case SC_POEMBRAGI:
 					case SC_APPLEIDUN:		case SC_HUMMING:		case SC_DONTFORGETME:
@@ -7940,6 +7941,7 @@ int skill_castend_nodamage_id (struct block_list *src, struct block_list *bl, ui
 					case SC_GOLDENMACECLAN:
 					case SC_CROSSBOWCLAN:
 					case SC_JUMPINGCLAN:
+					case SC_DAILYSENDMAILCNT:
 						continue;
 					case SC_WHISTLE:
 					case SC_ASSNCROS:
@@ -9337,6 +9339,7 @@ int skill_castend_nodamage_id (struct block_list *src, struct block_list *bl, ui
 					case SC_QUEST_BUFF1:	case SC_QUEST_BUFF2:	case SC_QUEST_BUFF3:
 					case SC_CLAN_INFO:		case SC_SWORDCLAN:		case SC_ARCWANDCLAN:
 					case SC_GOLDENMACECLAN:	case SC_CROSSBOWCLAN:
+					case SC_DAILYSENDMAILCNT:
 					continue;
 				case SC_ASSUMPTIO:
 					if( bl->type == BL_MOB )

+ 10 - 0
src/map/status.c

@@ -1116,6 +1116,9 @@ void initChangeTables(void)
     StatusIconChangeTable[SC_GEFFEN_MAGIC2] = SI_GEFFEN_MAGIC2;
     StatusIconChangeTable[SC_GEFFEN_MAGIC3] = SI_GEFFEN_MAGIC3;
 
+	// RODEX
+	StatusIconChangeTable[SC_DAILYSENDMAILCNT] = SI_DAILYSENDMAILCNT;
+
 	/* Other SC which are not necessarily associated to skills */
 	StatusChangeFlagTable[SC_ASPDPOTION0] |= SCB_ASPD;
 	StatusChangeFlagTable[SC_ASPDPOTION1] |= SCB_ASPD;
@@ -1260,6 +1263,9 @@ void initChangeTables(void)
 	StatusChangeFlagTable[SC_CROSSBOWCLAN] |= SCB_DEX|SCB_AGI|SCB_MAXHP|SCB_MAXSP;
 	StatusChangeFlagTable[SC_JUMPINGCLAN] |= SCB_STR|SCB_AGI|SCB_VIT|SCB_INT|SCB_DEX|SCB_LUK;
 
+	// RODEX
+	StatusChangeFlagTable[SC_DAILYSENDMAILCNT] |= SCB_NONE;
+
 #ifdef RENEWAL
 	// renewal EDP increases your weapon atk
 	StatusChangeFlagTable[SC_EDP] |= SCB_WATK;
@@ -9158,6 +9164,7 @@ int status_change_start(struct block_list* src, struct block_list* bl,enum sc_ty
 		case SC_PUSH_CART:
 		case SC_SPRITEMABLE:
 		case SC_CLAN_INFO:
+		case SC_DAILYSENDMAILCNT:
 			tick = -1;
 			break;
 
@@ -10898,6 +10905,7 @@ int status_change_start(struct block_list* src, struct block_list* bl,enum sc_ty
 		case SC_UPHEAVAL_OPTION:
 		case SC_CIRCLE_OF_FIRE_OPTION:
 		case SC_CLAN_INFO:
+		case SC_DAILYSENDMAILCNT:
 			val_flag |= 1|2;
 			break;
 		// Start |1|2|4 val_flag setting
@@ -11512,6 +11520,7 @@ int status_change_clear(struct block_list* bl, int type)
 			case SC_GOLDENMACECLAN:
 			case SC_CROSSBOWCLAN:
 			case SC_JUMPINGCLAN:
+			case SC_DAILYSENDMAILCNT:
 				continue;
 			}
 		}
@@ -11545,6 +11554,7 @@ int status_change_clear(struct block_list* bl, int type)
 			case SC_GOLDENMACECLAN:
 			case SC_CROSSBOWCLAN:
 			case SC_JUMPINGCLAN:
+			case SC_DAILYSENDMAILCNT:
 				continue;
 			}
 		}

+ 2 - 0
src/map/status.h

@@ -798,6 +798,8 @@ typedef enum sc_type {
 	SC_ARMOR_ELEMENT_FIRE,
 	SC_ARMOR_ELEMENT_WIND,
 
+	SC_DAILYSENDMAILCNT,
+
 #ifdef RENEWAL
 	SC_EXTREMITYFIST2, //! NOTE: This SC should be right before SC_MAX, so it doesn't disturb if RENEWAL is disabled
 #endif