// Copyright (c) 2014 The ShadowCoin developers // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include "main.h" #include "trianglesrpc.h" #include #include "smessage.h" #include "init.h" // pwalletMain #include using namespace json_spirit; using namespace std; extern void TxToJSON(const CTransaction& tx, const uint256 hashBlock, json_spirit::Object& entry); Value smsgenable(const Array& params, bool fHelp) { if (fHelp || params.size() != 0) throw runtime_error( "smsgenable \n" "Enable secure messaging."); if (fSecMsgEnabled) throw runtime_error("Secure messaging is already enabled."); Object result; if (!SecureMsgEnable()) { result.push_back(Pair("result", "Failed to enable secure messaging.")); } else { result.push_back(Pair("result", "Enabled secure messaging.")); } return result; } Value smsgdisable(const Array& params, bool fHelp) { if (fHelp || params.size() != 0) throw runtime_error( "smsgdisable \n" "Disable secure messaging."); if (!fSecMsgEnabled) throw runtime_error("Secure messaging is already disabled."); Object result; if (!SecureMsgDisable()) { result.push_back(Pair("result", "Failed to disable secure messaging.")); } else { result.push_back(Pair("result", "Disabled secure messaging.")); } return result; } Value smsgoptions(const Array& params, bool fHelp) { if (fHelp || params.size() > 3) throw runtime_error( "smsgoptions [list|set ]\n" "List and manage options."); std::string mode = "list"; if (params.size() > 0) { mode = params[0].get_str(); }; Object result; if (mode == "list") { result.push_back(Pair("option", std::string("newAddressRecv = ") + (smsgOptions.fNewAddressRecv ? "true" : "false"))); result.push_back(Pair("option", std::string("newAddressAnon = ") + (smsgOptions.fNewAddressAnon ? "true" : "false"))); result.push_back(Pair("result", "Success.")); } else if (mode == "set") { if (params.size() < 3) { result.push_back(Pair("result", "Too few parameters.")); result.push_back(Pair("expected", "set ")); return result; }; std::string optname = params[1].get_str(); std::string value = params[2].get_str(); if (optname == "newAddressRecv") { if (value == "+" || value == "on" || value == "true" || value == "1") { smsgOptions.fNewAddressRecv = true; } else if (value == "-" || value == "off" || value == "false" || value == "0") { smsgOptions.fNewAddressRecv = false; } else { result.push_back(Pair("result", "Unknown value.")); return result; }; result.push_back(Pair("set option", std::string("newAddressRecv = ") + (smsgOptions.fNewAddressRecv ? "true" : "false"))); } else if (optname == "newAddressAnon") { if (value == "+" || value == "on" || value == "true" || value == "1") { smsgOptions.fNewAddressAnon = true; } else if (value == "-" || value == "off" || value == "false" || value == "0") { smsgOptions.fNewAddressAnon = false; } else { result.push_back(Pair("result", "Unknown value.")); return result; }; result.push_back(Pair("set option", std::string("newAddressAnon = ") + (smsgOptions.fNewAddressAnon ? "true" : "false"))); } else { result.push_back(Pair("result", "Option not found.")); return result; }; } else { result.push_back(Pair("result", "Unknown Mode.")); result.push_back(Pair("expected", "smsgoption [list|set ]")); }; return result; } Value smsglocalkeys(const Array& params, bool fHelp) { if (fHelp || params.size() > 3) throw runtime_error( "smsglocalkeys [whitelist|all|wallet|recv <+/->
|anon <+/->
]\n" "List and manage keys."); if (!fSecMsgEnabled) throw runtime_error("Secure messaging is disabled."); Object result; std::string mode = "whitelist"; if (params.size() > 0) { mode = params[0].get_str(); }; char cbuf[256]; if (mode == "whitelist" || mode == "all") { uint32_t nKeys = 0; int all = mode == "all" ? 1 : 0; for (std::vector::iterator it = smsgAddresses.begin(); it != smsgAddresses.end(); ++it) { if (!all && !it->fReceiveEnabled) continue; CTrianglesAddress coinAddress(it->sAddress); if (!coinAddress.IsValid()) continue; std::string sPublicKey; CKeyID keyID; if (!coinAddress.GetKeyID(keyID)) continue; CPubKey pubKey; if (!pwalletMain->GetPubKey(keyID, pubKey)) continue; if (!pubKey.IsValid() || !pubKey.IsCompressed()) { continue; }; sPublicKey = EncodeBase58(pubKey.Raw()); std::string sLabel = pwalletMain->mapAddressBook[keyID]; std::string sInfo; if (all) sInfo = std::string("Receive ") + (it->fReceiveEnabled ? "on, " : "off, "); sInfo += std::string("Anon ") + (it->fReceiveAnon ? "on" : "off"); result.push_back(Pair("key", it->sAddress + " - " + sPublicKey + " " + sInfo + " - " + sLabel)); nKeys++; }; snprintf(cbuf, sizeof(cbuf), "%u keys listed.", nKeys); result.push_back(Pair("result", std::string(cbuf))); } else if (mode == "recv") { if (params.size() < 3) { result.push_back(Pair("result", "Too few parameters.")); result.push_back(Pair("expected", "recv <+/->
")); return result; }; std::string op = params[1].get_str(); std::string addr = params[2].get_str(); std::vector::iterator it; for (it = smsgAddresses.begin(); it != smsgAddresses.end(); ++it) { if (addr != it->sAddress) continue; break; }; if (it == smsgAddresses.end()) { result.push_back(Pair("result", "Address not found.")); return result; }; if (op == "+" || op == "on" || op == "add" || op == "a") { it->fReceiveEnabled = true; } else if (op == "-" || op == "off" || op == "rem" || op == "r") { it->fReceiveEnabled = false; } else { result.push_back(Pair("result", "Unknown operation.")); return result; }; std::string sInfo; sInfo = std::string("Receive ") + (it->fReceiveEnabled ? "on, " : "off,"); sInfo += std::string("Anon ") + (it->fReceiveAnon ? "on" : "off"); result.push_back(Pair("result", "Success.")); result.push_back(Pair("key", it->sAddress + " " + sInfo)); return result; } else if (mode == "anon") { if (params.size() < 3) { result.push_back(Pair("result", "Too few parameters.")); result.push_back(Pair("expected", "anon <+/->
")); return result; }; std::string op = params[1].get_str(); std::string addr = params[2].get_str(); std::vector::iterator it; for (it = smsgAddresses.begin(); it != smsgAddresses.end(); ++it) { if (addr != it->sAddress) continue; break; }; if (it == smsgAddresses.end()) { result.push_back(Pair("result", "Address not found.")); return result; }; if (op == "+" || op == "on" || op == "add" || op == "a") { it->fReceiveAnon = true; } else if (op == "-" || op == "off" || op == "rem" || op == "r") { it->fReceiveAnon = false; } else { result.push_back(Pair("result", "Unknown operation.")); return result; }; std::string sInfo; sInfo = std::string("Receive ") + (it->fReceiveEnabled ? "on, " : "off,"); sInfo += std::string("Anon ") + (it->fReceiveAnon ? "on" : "off"); result.push_back(Pair("result", "Success.")); result.push_back(Pair("key", it->sAddress + " " + sInfo)); return result; } else if (mode == "wallet") { uint32_t nKeys = 0; for (const auto& entry : pwalletMain->mapAddressBook) { if (!IsMine(*pwalletMain, entry.first)) continue; CTrianglesAddress coinAddress(entry.first); if (!coinAddress.IsValid()) continue; std::string address; std::string sPublicKey; address = coinAddress.ToString(); CKeyID keyID; if (!coinAddress.GetKeyID(keyID)) continue; CPubKey pubKey; if (!pwalletMain->GetPubKey(keyID, pubKey)) continue; if (!pubKey.IsValid() || !pubKey.IsCompressed()) { continue; }; sPublicKey = EncodeBase58(pubKey.Raw()); result.push_back(Pair("key", address + " - " + sPublicKey + " - " + entry.second)); nKeys++; }; snprintf(cbuf, sizeof(cbuf), "%u keys listed from wallet.", nKeys); result.push_back(Pair("result", std::string(cbuf))); } else { result.push_back(Pair("result", "Unknown Mode.")); result.push_back(Pair("expected", "smsglocalkeys [whitelist|all|wallet|recv <+/->
|anon <+/->
]")); }; return result; }; Value smsgscanchain(const Array& params, bool fHelp) { if (fHelp || params.size() != 0) throw runtime_error( "smsgscanchain \n" "Look for public keys in the block chain."); if (!fSecMsgEnabled) throw runtime_error("Secure messaging is disabled."); Object result; if (!SecureMsgScanBlockChain()) { result.push_back(Pair("result", "Scan Chain Failed.")); } else { result.push_back(Pair("result", "Scan Chain Completed.")); } return result; } Value smsgscanbuckets(const Array& params, bool fHelp) { if (fHelp || params.size() != 0) throw runtime_error( "smsgscanbuckets \n" "Force rescan of all messages in the bucket store."); if (!fSecMsgEnabled) throw runtime_error("Secure messaging is disabled."); if (pwalletMain->IsLocked()) throw runtime_error("Wallet is locked."); Object result; if (!SecureMsgScanBuckets()) { result.push_back(Pair("result", "Scan Buckets Failed.")); } else { result.push_back(Pair("result", "Scan Buckets Completed.")); } return result; } Value smsgaddkey(const Array& params, bool fHelp) { if (fHelp || params.size() != 2) throw runtime_error( "smsgaddkey
\n" "Add address, pubkey pair to database."); if (!fSecMsgEnabled) throw runtime_error("Secure messaging is disabled."); std::string addr = params[0].get_str(); std::string pubk = params[1].get_str(); Object result; int rv = SecureMsgAddAddress(addr, pubk); if (rv != 0) { result.push_back(Pair("result", "Public key not added to db.")); switch (rv) { case 2: result.push_back(Pair("reason", "publicKey is invalid.")); break; case 3: result.push_back(Pair("reason", "publicKey does not match address.")); break; case 4: result.push_back(Pair("reason", "address is already in db.")); break; case 5: result.push_back(Pair("reason", "address is invalid.")); break; default: result.push_back(Pair("reason", "error.")); break; }; } else { result.push_back(Pair("result", "Added public key to db.")); }; return result; } Value smsggetpubkey(const Array& params, bool fHelp) { if (fHelp || params.size() != 1) throw runtime_error( "smsggetpubkey
\n" "Return the base58 encoded compressed public key for an address.\n" "Tests localkeys first, then looks in public key db.\n"); if (!fSecMsgEnabled) throw runtime_error("Secure messaging is disabled."); std::string address = params[0].get_str(); std::string publicKey; Object result; int rv = SecureMsgGetLocalPublicKey(address, publicKey); switch (rv) { case 0: result.push_back(Pair("result", "Success.")); result.push_back(Pair("address in wallet", address)); result.push_back(Pair("compressed public key", publicKey)); return result; // success, don't check db case 2: case 3: result.push_back(Pair("result", "Failed.")); result.push_back(Pair("message", "Invalid address.")); return result; case 4: break; // check db //case 1: default: result.push_back(Pair("result", "Failed.")); result.push_back(Pair("message", "Error.")); return result; }; CTrianglesAddress coinAddress(address); CKeyID keyID; if (!coinAddress.GetKeyID(keyID)) { result.push_back(Pair("result", "Failed.")); result.push_back(Pair("message", "Invalid address.")); return result; }; CPubKey cpkFromDB; rv = SecureMsgGetStoredKey(keyID, cpkFromDB); switch (rv) { case 0: if (!cpkFromDB.IsValid() || !cpkFromDB.IsCompressed()) { result.push_back(Pair("result", "Failed.")); result.push_back(Pair("message", "Invalid address.")); } else { //cpkFromDB.SetCompressedPubKey(); // make sure key is compressed publicKey = EncodeBase58(cpkFromDB.Raw()); result.push_back(Pair("result", "Success.")); result.push_back(Pair("peer address in DB", address)); result.push_back(Pair("compressed public key", publicKey)); }; break; case 2: result.push_back(Pair("result", "Failed.")); result.push_back(Pair("message", "Address not found in wallet or db.")); return result; //case 1: default: result.push_back(Pair("result", "Failed.")); result.push_back(Pair("message", "Error, GetStoredKey().")); return result; }; return result; } Value smsgsend(const Array& params, bool fHelp) { if (fHelp || params.size() != 3) throw runtime_error( "smsgsend \n" "Send an encrypted message from addrFrom to addrTo."); if (!fSecMsgEnabled) throw runtime_error("Secure messaging is disabled."); std::string addrFrom = params[0].get_str(); std::string addrTo = params[1].get_str(); std::string msg = params[2].get_str(); Object result; std::string sError; if (SecureMsgSend(addrFrom, addrTo, msg, sError) != 0) { result.push_back(Pair("result", "Send failed.")); result.push_back(Pair("error", sError)); } else result.push_back(Pair("result", "Sent.")); return result; } Value smsgsendanon(const Array& params, bool fHelp) { if (fHelp || params.size() != 2) throw runtime_error( "smsgsendanon \n" "Send an anonymous encrypted message to addrTo."); if (!fSecMsgEnabled) throw runtime_error("Secure messaging is disabled."); std::string addrFrom = "anon"; std::string addrTo = params[0].get_str(); std::string msg = params[1].get_str(); Object result; std::string sError; if (SecureMsgSend(addrFrom, addrTo, msg, sError) != 0) { result.push_back(Pair("result", "Send failed.")); result.push_back(Pair("error", sError)); } else result.push_back(Pair("result", "Sent.")); return result; } Value smsginbox(const Array& params, bool fHelp) { if (fHelp || params.size() > 1) // defaults to read throw runtime_error( "smsginbox [all|unread|clear]\n" "Decrypt and display all received messages.\n" "Warning: clear will delete all messages."); if (!fSecMsgEnabled) throw runtime_error("Secure messaging is disabled."); if (pwalletMain->IsLocked()) throw runtime_error("Wallet is locked."); std::string mode = "unread"; if (params.size() > 0) { mode = params[0].get_str(); } Object result; std::vector vchKey; vchKey.resize(16); memset(&vchKey[0], 0, 16); { LOCK(cs_smsgDB); SecMsgDB dbInbox; if (!dbInbox.Open("cr+")) throw runtime_error("Could not open DB."); uint32_t nMessages = 0; char cbuf[256]; std::string sPrefix("im"); unsigned char chKey[18]; if (mode == "clear") { dbInbox.TxnBegin(); rocksdb::Iterator* it = dbInbox.pdb->NewIterator(rocksdb::ReadOptions()); while (dbInbox.NextSmesgKey(it, sPrefix, chKey)) { dbInbox.EraseSmesg(chKey); nMessages++; }; delete it; dbInbox.TxnCommit(); snprintf(cbuf, sizeof(cbuf), "Deleted %u messages.", nMessages); result.push_back(Pair("result", std::string(cbuf))); } else if (mode == "all" || mode == "unread") { int fCheckReadStatus = mode == "unread" ? 1 : 0; SecMsgStored smsgStored; MessageData msg; dbInbox.TxnBegin(); rocksdb::Iterator* it = dbInbox.pdb->NewIterator(rocksdb::ReadOptions()); while (dbInbox.NextSmesg(it, sPrefix, chKey, smsgStored)) { if (fCheckReadStatus && !(smsgStored.status & SMSG_MASK_UNREAD)) continue; uint32_t nPayload = smsgStored.vchMessage.size() - SMSG_HDR_LEN; if (SecureMsgDecrypt(false, smsgStored.sAddrTo, &smsgStored.vchMessage[0], &smsgStored.vchMessage[SMSG_HDR_LEN], nPayload, msg) == 0) { Object objM; objM.push_back(Pair("received", getTimeString(smsgStored.timeReceived, cbuf, sizeof(cbuf)))); objM.push_back(Pair("sent", getTimeString(msg.timestamp, cbuf, sizeof(cbuf)))); objM.push_back(Pair("from", msg.sFromAddress)); objM.push_back(Pair("to", smsgStored.sAddrTo)); objM.push_back(Pair("text", std::string((char*)&msg.vchMessage[0]))); // ugh result.push_back(Pair("message", objM)); } else { result.push_back(Pair("message", "Could not decrypt.")); }; if (fCheckReadStatus) { smsgStored.status &= ~SMSG_MASK_UNREAD; dbInbox.WriteSmesg(chKey, smsgStored); }; nMessages++; }; delete it; dbInbox.TxnCommit(); snprintf(cbuf, sizeof(cbuf), "%u messages shown.", nMessages); result.push_back(Pair("result", std::string(cbuf))); } else { result.push_back(Pair("result", "Unknown Mode.")); result.push_back(Pair("expected", "[all|unread|clear].")); }; } return result; }; Value smsgoutbox(const Array& params, bool fHelp) { if (fHelp || params.size() > 1) // defaults to read throw runtime_error( "smsgoutbox [all|clear]\n" "Decrypt and display all sent messages.\n" "Warning: clear will delete all sent messages."); if (!fSecMsgEnabled) throw runtime_error("Secure messaging is disabled."); if (pwalletMain->IsLocked()) throw runtime_error("Wallet is locked."); std::string mode = "all"; if (params.size() > 0) { mode = params[0].get_str(); } Object result; std::string sPrefix("sm"); unsigned char chKey[18]; memset(&chKey[0], 0, 18); { LOCK(cs_smsgDB); SecMsgDB dbOutbox; if (!dbOutbox.Open("cr+")) throw runtime_error("Could not open DB."); uint32_t nMessages = 0; char cbuf[256]; if (mode == "clear") { dbOutbox.TxnBegin(); rocksdb::Iterator* it = dbOutbox.pdb->NewIterator(rocksdb::ReadOptions()); while (dbOutbox.NextSmesgKey(it, sPrefix, chKey)) { dbOutbox.EraseSmesg(chKey); nMessages++; }; delete it; dbOutbox.TxnCommit(); snprintf(cbuf, sizeof(cbuf), "Deleted %u messages.", nMessages); result.push_back(Pair("result", std::string(cbuf))); } else if (mode == "all") { SecMsgStored smsgStored; MessageData msg; rocksdb::Iterator* it = dbOutbox.pdb->NewIterator(rocksdb::ReadOptions()); while (dbOutbox.NextSmesg(it, sPrefix, chKey, smsgStored)) { uint32_t nPayload = smsgStored.vchMessage.size() - SMSG_HDR_LEN; if (SecureMsgDecrypt(false, smsgStored.sAddrOutbox, &smsgStored.vchMessage[0], &smsgStored.vchMessage[SMSG_HDR_LEN], nPayload, msg) == 0) { Object objM; objM.push_back(Pair("sent", getTimeString(msg.timestamp, cbuf, sizeof(cbuf)))); objM.push_back(Pair("from", msg.sFromAddress)); objM.push_back(Pair("to", smsgStored.sAddrTo)); objM.push_back(Pair("text", std::string((char*)&msg.vchMessage[0]))); // ugh result.push_back(Pair("message", objM)); } else { result.push_back(Pair("message", "Could not decrypt.")); }; nMessages++; }; delete it; snprintf(cbuf, sizeof(cbuf), "%u sent messages shown.", nMessages); result.push_back(Pair("result", std::string(cbuf))); } else { result.push_back(Pair("result", "Unknown Mode.")); result.push_back(Pair("expected", "[all|clear].")); }; } return result; }; Value smsgbuckets(const Array& params, bool fHelp) { if (fHelp || params.size() > 1) throw runtime_error( "smsgbuckets [stats|dump]\n" "Display some statistics."); if (!fSecMsgEnabled) throw runtime_error("Secure messaging is disabled."); std::string mode = "stats"; if (params.size() > 0) { mode = params[0].get_str(); }; Object result; char cbuf[256]; if (mode == "stats") { uint32_t nBuckets = 0; uint32_t nMessages = 0; uint64_t nBytes = 0; { LOCK(cs_smsg); std::map::iterator it; it = smsgBuckets.begin(); for (it = smsgBuckets.begin(); it != smsgBuckets.end(); ++it) { std::set& tokenSet = it->second.setTokens; std::string sBucket = std::to_string(it->first); std::string sFile = sBucket + "_01.dat"; snprintf(cbuf, sizeof(cbuf), "%"PRIszu, tokenSet.size()); std::string snContents(cbuf); std::string sHash = std::to_string(it->second.hash); nBuckets++; nMessages += tokenSet.size(); Object objM; objM.push_back(Pair("bucket", sBucket)); objM.push_back(Pair("time", getTimeString(it->first, cbuf, sizeof(cbuf)))); objM.push_back(Pair("no. messages", snContents)); objM.push_back(Pair("hash", sHash)); objM.push_back(Pair("last changed", getTimeString(it->second.timeChanged, cbuf, sizeof(cbuf)))); std::filesystem::path fullPath = GetDataDir() / "smsgStore" / sFile; if (!std::filesystem::exists(fullPath)) { // -- If there is a file for an empty bucket something is wrong. if (tokenSet.size() == 0) objM.push_back(Pair("file size", "Empty bucket.")); else objM.push_back(Pair("file size, error", "File not found.")); } else { try { uint64_t nFBytes = 0; nFBytes = std::filesystem::file_size(fullPath); nBytes += nFBytes; objM.push_back(Pair("file size", fsReadable(nFBytes))); } catch (const std::filesystem::filesystem_error& ex) { objM.push_back(Pair("file size, error", ex.what())); }; }; result.push_back(Pair("bucket", objM)); }; }; // LOCK(cs_smsg); std::string snBuckets = std::to_string(nBuckets); std::string snMessages = std::to_string(nMessages); Object objM; objM.push_back(Pair("buckets", snBuckets)); objM.push_back(Pair("messages", snMessages)); objM.push_back(Pair("size", fsReadable(nBytes))); result.push_back(Pair("total", objM)); } else if (mode == "dump") { { LOCK(cs_smsg); std::map::iterator it; it = smsgBuckets.begin(); for (it = smsgBuckets.begin(); it != smsgBuckets.end(); ++it) { std::string sFile = std::to_string(it->first) + "_01.dat"; try { std::filesystem::path fullPath = GetDataDir() / "smsgStore" / sFile; std::filesystem::remove(fullPath); } catch (const std::filesystem::filesystem_error& ex) { //objM.push_back(Pair("file size, error", ex.what())); printf("Error removing bucket file %s.\n", ex.what()); }; }; smsgBuckets.clear(); }; // LOCK(cs_smsg); result.push_back(Pair("result", "Removed all buckets.")); } else { result.push_back(Pair("result", "Unknown Mode.")); result.push_back(Pair("expected", "[stats|dump].")); }; return result; }; Value smsgbroadcast(const Array& params, bool fHelp) { if (fHelp || params.size() != 2) throw runtime_error( "smsgbroadcast \n" "Send an encrypted message to all known public keys in the smsgDB.\n" "Returns the number of recipients and any failures."); if (!fSecMsgEnabled) throw runtime_error("Secure messaging is disabled."); if (pwalletMain->IsLocked()) throw runtime_error("Wallet is locked."); std::string addrFrom = params[0].get_str(); std::string msg = params[1].get_str(); // Validate sender address CTrianglesAddress coinAddrFrom(addrFrom); if (!coinAddrFrom.IsValid()) throw runtime_error("Invalid from address."); Object result; uint32_t nSent = 0; uint32_t nFailed = 0; Array failures; { LOCK(cs_smsgDB); SecMsgDB dbPub; if (!dbPub.Open("r")) throw runtime_error("Could not open smsgDB."); // Iterate all "pk" entries std::string sPrefix("pk"); rocksdb::Iterator* it = dbPub.pdb->NewIterator(rocksdb::ReadOptions()); for (it->Seek(sPrefix); it->Valid(); it->Next()) { std::string key = it->key().ToString(); if (key.size() < 2 || key[0] != 'p' || key[1] != 'k') break; // Deserialize the CKeyID from the key (after "pk" prefix) CDataStream ssKey(key.data(), key.data() + key.size(), SER_DISK, CLIENT_VERSION); char prefix[2]; ssKey >> prefix[0]; ssKey >> prefix[1]; CKeyID ckidTo; ssKey >> ckidTo; // Convert CKeyID to address string CTrianglesAddress addrTo(ckidTo); if (!addrTo.IsValid()) continue; std::string sAddrTo = addrTo.ToString(); // Skip sending to self if (sAddrTo == addrFrom) continue; std::string sError; if (SecureMsgSend(addrFrom, sAddrTo, msg, sError) != 0) { nFailed++; Object objFail; objFail.push_back(Pair("address", sAddrTo)); objFail.push_back(Pair("error", sError)); failures.push_back(objFail); } else { nSent++; } } delete it; } char cbuf[256]; snprintf(cbuf, sizeof(cbuf), "Broadcast sent to %u recipients, %u failed.", nSent, nFailed); result.push_back(Pair("result", std::string(cbuf))); result.push_back(Pair("sent", (int)nSent)); result.push_back(Pair("failed", (int)nFailed)); if (nFailed > 0) result.push_back(Pair("failures", failures)); return result; };