diff options
Diffstat (limited to 'src/uscxml/plugins')
-rw-r--r-- | src/uscxml/plugins/datamodel/prolog/swi/SWIDataModel.cpp | 23 | ||||
-rw-r--r-- | src/uscxml/plugins/element/file/FileElement.cpp | 45 | ||||
-rw-r--r-- | src/uscxml/plugins/element/file/FileElement.h | 2 | ||||
-rw-r--r-- | src/uscxml/plugins/invoker/CMakeLists.txt | 20 | ||||
-rw-r--r-- | src/uscxml/plugins/invoker/smtp/SMTPInvoker.cpp | 360 | ||||
-rw-r--r-- | src/uscxml/plugins/invoker/smtp/SMTPInvoker.h | 78 |
6 files changed, 500 insertions, 28 deletions
diff --git a/src/uscxml/plugins/datamodel/prolog/swi/SWIDataModel.cpp b/src/uscxml/plugins/datamodel/prolog/swi/SWIDataModel.cpp index 9a4b528..de94088 100644 --- a/src/uscxml/plugins/datamodel/prolog/swi/SWIDataModel.cpp +++ b/src/uscxml/plugins/datamodel/prolog/swi/SWIDataModel.cpp @@ -627,20 +627,19 @@ std::string SWIDataModel::evalAsString(const std::string& expr) { PlQuery query(compound.name(), termv); std::stringstream ss; - const char* separator = ""; + std::string solSeparator = ""; while (query.next_solution()) { + ss << solSeparator; std::map<std::string, PlTerm> vars = resolveAtoms(compound, orig); - if (vars.size() == 1) { - ss << separator << (char *)vars.begin()->second; - separator = "\n"; - } else { - std::map<std::string, PlTerm>::const_iterator varIter = vars.begin(); - while(varIter != vars.end()) { - ss << separator << (char *)varIter->second; - separator = ", "; - varIter++; - } + std::map<std::string, PlTerm>::const_iterator varIter = vars.begin(); + + std::string varSeparator = ""; + while(varIter != vars.end()) { + ss << varSeparator << (char *)varIter->second; + varSeparator = ", "; + varIter++; } + solSeparator = "\n"; } return ss.str(); } @@ -684,6 +683,8 @@ std::map<std::string, PlTerm> SWIDataModel::resolveAtoms(PlTerm& term, PlTerm& o atoms.insert(result.begin(), result.end()); } break; + default: + LOG(ERROR) << "Resolving variable of unknown type in query solution"; } return atoms; } RETHROW_PLEX_AS_EVENT diff --git a/src/uscxml/plugins/element/file/FileElement.cpp b/src/uscxml/plugins/element/file/FileElement.cpp index 3a8574d..d88a598 100644 --- a/src/uscxml/plugins/element/file/FileElement.cpp +++ b/src/uscxml/plugins/element/file/FileElement.cpp @@ -134,8 +134,9 @@ void FileElement::enterElement(const Arabica::DOM::Node<std::string>& node) { if (_sandBoxed) _actualUrl.toAbsolute(URL::getResourceDir()); - _filename = _actualUrl.path(); - + _filepath = _actualUrl.path(); + + std::string writeMode; switch (_operation) { case APPEND: @@ -145,39 +146,43 @@ void FileElement::enterElement(const Arabica::DOM::Node<std::string>& node) { writeMode = "w+"; FILE *fp; - fp = fopen(_filename.c_str(), writeMode.c_str()); + fp = fopen(_filepath.c_str(), writeMode.c_str()); if (fp == NULL) { - LOG(ERROR) << "Error opening '" << _filename << "' for writing: " << strerror(errno); + LOG(ERROR) << "Error opening '" << _filepath << "' for writing: " << strerror(errno); } if (content && contentSize > 0) { size_t written = fwrite(content, 1, contentSize, fp); if (written != contentSize) { - LOG(ERROR) << "Error writing to '" << _filename << "': " << strerror(errno); + LOG(ERROR) << "Error writing to '" << _filepath << "': " << strerror(errno); return; } } else if (contentStr.length() > 0) { size_t written = fwrite(contentStr.c_str(), contentStr.length(), 1, fp); if (written < 1) { - LOG(ERROR) << "Error writing to '" << _filename << "': " << strerror(errno); + LOG(ERROR) << "Error writing to '" << _filepath << "': " << strerror(errno); } } else { - LOG(WARNING) << "Nothing to write to '" << _filename; + LOG(WARNING) << "Nothing to write to '" << _filepath; } fclose(fp); break; } case READ: { struct stat fileStat; - int err = stat(_filename.c_str(), &fileStat); + int err = stat(_filepath.c_str(), &fileStat); if (err < 0) { - LOG(ERROR) << "Cannot stat file '" << _filename << "': " << strerror(errno); + LOG(ERROR) << "Cannot stat file '" << _filepath << "': " << strerror(errno); return; } Event event; event.name = callback; - event.data.compound["file"].compound["name"] = Data(_filename, Data::VERBATIM); + + std::string filename = _actualUrl.pathComponents()[_actualUrl.pathComponents().size() - 1]; + + event.data.compound["file"].compound["name"] = Data(filename, Data::VERBATIM); + event.data.compound["file"].compound["path"] = Data(_filepath, Data::VERBATIM); event.data.compound["file"].compound["mtime"] = toStr(fileStat.st_mtime); event.data.compound["file"].compound["ctime"] = toStr(fileStat.st_ctime); event.data.compound["file"].compound["atime"] = toStr(fileStat.st_atime); @@ -185,7 +190,7 @@ void FileElement::enterElement(const Arabica::DOM::Node<std::string>& node) { FILE *fp; - fp = fopen(_filename.c_str(), "r"); + fp = fopen(_filepath.c_str(), "r"); fseek (fp, 0, SEEK_END); size_t filesize = ftell(fp); @@ -195,14 +200,22 @@ void FileElement::enterElement(const Arabica::DOM::Node<std::string>& node) { size_t read = fread(fileContents, 1, filesize, fp); fclose(fp); if (read != filesize) { - LOG(ERROR) << "Error reading from '" << _filename << "': " << strerror(errno); + LOG(ERROR) << "Error reading from '" << _filepath << "': " << strerror(errno); return; } switch (_type) { - case BINARY: - event.data.compound["content"] = Data(fileContents, fileStat.st_size, "application/octet-stream", true); + case BINARY: { + std::string mimetype = "application/octet-stream"; + if (HAS_ATTR(node, "mimetype")) { + mimetype = ATTR(node, "mimetype"); + } else if(HAS_ATTR(node, "mimetypeexpr")) { + mimetype = _interpreter->getDataModel().evalAsString(ATTR(node, "mimetypeexpr")); + } + + event.data.compound["content"] = Data(fileContents, fileStat.st_size, mimetype, true); break; + } case TEXT: event.data.compound["content"] = Data(fileContents, Data::VERBATIM); free(fileContents); @@ -211,7 +224,7 @@ void FileElement::enterElement(const Arabica::DOM::Node<std::string>& node) { Data json = Data::fromJSON(fileContents); free(fileContents); if (!json) { - LOG(ERROR) << "Cannot parse contents of " << _filename << " as JSON"; + LOG(ERROR) << "Cannot parse contents of " << _filepath << " as JSON"; return; } event.data.compound["content"] = json; @@ -220,7 +233,7 @@ void FileElement::enterElement(const Arabica::DOM::Node<std::string>& node) { case XML: { NameSpacingParser parser = NameSpacingParser::fromXML(fileContents); if (parser.errorsReported()) { - LOG(ERROR) << "Cannot parse contents of " << _filename << " as XML"; + LOG(ERROR) << "Cannot parse contents of " << _filepath << " as XML"; return; } event.dom = parser.getDocument().getDocumentElement(); diff --git a/src/uscxml/plugins/element/file/FileElement.h b/src/uscxml/plugins/element/file/FileElement.h index 987ae11..462ccc8 100644 --- a/src/uscxml/plugins/element/file/FileElement.h +++ b/src/uscxml/plugins/element/file/FileElement.h @@ -70,7 +70,7 @@ protected: bool _sandBoxed; std::string _givenUrl; URL _actualUrl; - std::string _filename; + std::string _filepath; Operation _operation; Type _type; }; diff --git a/src/uscxml/plugins/invoker/CMakeLists.txt b/src/uscxml/plugins/invoker/CMakeLists.txt index c6ec719..e731d74 100644 --- a/src/uscxml/plugins/invoker/CMakeLists.txt +++ b/src/uscxml/plugins/invoker/CMakeLists.txt @@ -117,6 +117,26 @@ if (EXPECT_FOUND AND TCL_FOUND) endif() +# SMTP invoker via curl + +set(USCXML_INVOKERS "smtp ${USCXML_INVOKERS}") +file(GLOB_RECURSE SMTP_INVOKER + smtp/*.cpp + smtp/*.h +) +if (BUILD_AS_PLUGINS) + source_group("" FILES SMTP_INVOKER) + add_library( + invoker_smtp SHARED + ${SMTP_INVOKER} + "../Plugins.cpp") + target_link_libraries(invoker_smtp uscxml) + set_target_properties(invoker_smtp PROPERTIES FOLDER "Plugin Invoker") +else() + list (APPEND USCXML_FILES ${SMTP_INVOKER}) +endif() + + # SQLite3 SQL Invoker if (SQLITE3_FOUND) diff --git a/src/uscxml/plugins/invoker/smtp/SMTPInvoker.cpp b/src/uscxml/plugins/invoker/smtp/SMTPInvoker.cpp new file mode 100644 index 0000000..420ab52 --- /dev/null +++ b/src/uscxml/plugins/invoker/smtp/SMTPInvoker.cpp @@ -0,0 +1,360 @@ +/** + * @file + * @author 2012-2013 Stefan Radomski (stefan.radomski@cs.tu-darmstadt.de) + * @copyright Simplified BSD + * + * @cond + * This program is free software: you can redistribute it and/or modify + * it under the terms of the FreeBSD license as published by the FreeBSD + * project. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the FreeBSD license along with this + * program. If not, see <http://www.opensource.org/licenses/bsd-license>. + * @endcond + */ + +#include "SMTPInvoker.h" +#include <glog/logging.h> + +#ifdef BUILD_AS_PLUGINS +#include <Pluma/Connector.hpp> +#endif + +#include <boost/algorithm/string.hpp> +#include "uscxml/UUID.h" + +namespace uscxml { + +#ifdef BUILD_AS_PLUGINS +PLUMA_CONNECTOR +bool pluginConnect(pluma::Host& host) { + host.add( new SMTPInvokerProvider() ); + return true; +} +#endif + +SMTPInvoker::SMTPInvoker() { +} + +SMTPInvoker::~SMTPInvoker() { +}; + +boost::shared_ptr<InvokerImpl> SMTPInvoker::create(InterpreterImpl* interpreter) { + boost::shared_ptr<SMTPInvoker> invoker = boost::shared_ptr<SMTPInvoker>(new SMTPInvoker()); + return invoker; +} + +Data SMTPInvoker::getDataModelVariables() { + Data data; + return data; +} + +size_t SMTPInvoker::readCurlData(void *ptr, size_t size, size_t nmemb, void *userdata) { + if (!userdata) + return 0; + + SMTPContext* ctx = (SMTPContext*)userdata; + + size_t toWrite = std::min(ctx->content.length() - ctx->readPtr, size * nmemb); + if (toWrite > 0) { + memcpy (ptr, ctx->content.c_str() + ctx->readPtr, toWrite); + ctx->readPtr += toWrite; + } + + return toWrite; +} + +std::list<std::string> SMTPInvoker::getAtoms(std::list<Data> list) { + std::list<std::string> atoms; + + std::list<Data>::const_iterator iter = list.begin(); + while(iter != list.end()) { + const Data& data = *iter; + if (data.atom.size() > 0) { + atoms.push_back(data.atom); + } else if (data.array.size() > 0) { + std::list<Data>::const_iterator arrIter = data.array.begin(); + while(arrIter != data.array.end()) { + if (arrIter->atom.size() > 0) { + atoms.push_back(arrIter->atom); + arrIter++; + } + } + } + iter++; + } + return atoms; +} + +void SMTPInvoker::getAttachments(std::list<Data> list, std::list<Data>& attachments) { + // accumulate attachments with filename, mimetype and data + std::list<Data>::const_iterator iter = list.begin(); + while(iter != list.end()) { + const Data& data = *iter; + if (data.hasKey("data")) { + // compound structure with all information + Data att = data; + + if (!att.hasKey("mimetype")) { + if (att["data"].binary && att["data"].binary->mimeType.size() > 0) { + att.compound["mimetype"] = Data(att["data"].binary->mimeType, Data::VERBATIM); + } else { + att.compound["mimetype"] = Data("text/plain", Data::VERBATIM); + } + } + + if (!att.hasKey("filename")) { + std::stringstream filenameSS; + filenameSS << "attachment" << attachments.size() + 1; + if (boost::starts_with(att.compound["mimetype"].atom, "text")) { + filenameSS << ".txt"; + } else { + filenameSS << ".bin"; + } + att.compound["filename"] = Data(filenameSS.str(), Data::VERBATIM); + } + + attachments.push_back(att); + + } else if (data.binary) { + // a single binary blob + Data att; + + att.compound["data"].binary = data.binary; + + if (data.binary->mimeType.size() > 0) { + att.compound["mimetype"] = Data(attachments.back()["data"].binary->mimeType, Data::VERBATIM); + } else { + att.compound["mimetype"] = Data("application/octet-stream", Data::VERBATIM); + } + + std::stringstream filenameSS; + filenameSS << "attachment" << attachments.size() + 1; + if (boost::starts_with(att.compound["mimetype"].atom, "text")) { + filenameSS << ".txt"; + } else { + filenameSS << ".bin"; + } + att.compound["filename"] = Data(filenameSS.str(), Data::VERBATIM); + + attachments.push_back(att); + + } else if (data.compound.size() > 0) { + // data is some compound, descent to find attachment structures or binaries + std::map<std::string, Data>::const_iterator compIter = data.compound.begin(); + while(compIter != data.compound.end()) { + std::list<Data> tmp; + tmp.push_back(compIter->second); + getAttachments(tmp, attachments); + compIter++; + } + } else if (data.array.size() > 0) { + // descent into array + getAttachments(data.array, attachments); + } + iter++; + } +} + +void SMTPInvoker::send(const SendRequest& req) { + if (iequals(req.name, "mail.send")) { + + struct curl_slist* recipients = NULL; + CURLcode curlError; + std::string multipartSep; + + bool verbose; + std::string from; + std::string subject; + std::string contentType; + std::list<Data> headerParams; + std::list<Data> toParams; + std::list<Data> ccParams; + std::list<Data> bccParams; + std::list<Data> attachmentParams; + + Event::getParam(req.params, "verbose", verbose); + Event::getParam(req.params, "Content-Type", contentType); + Event::getParam(req.params, "attachment", attachmentParams); + Event::getParam(req.params, "from", from); + Event::getParam(req.params, "subject", subject); + Event::getParam(req.params, "header", headerParams); + Event::getParam(req.params, "to", toParams); + Event::getParam(req.params, "cc", ccParams); + Event::getParam(req.params, "bcc", bccParams); + + if (contentType.size() == 0) + contentType = "text/plain; charset=\"UTF-8\""; + + SMTPContext* ctx = new SMTPContext(); + std::stringstream contentSS; + + std::list<std::string>::const_iterator recIter; + std::list<std::string> to = getAtoms(toParams); + std::list<std::string> cc = getAtoms(ccParams); + std::list<std::string> bcc = getAtoms(bccParams); + std::list<std::string> headers = getAtoms(headerParams); + std::list<Data> attachments; getAttachments(attachmentParams, attachments); + + if (to.size() == 0) + return; + + recIter = to.begin(); + recIter++; // skip first as we need it in CURLOPT_MAIL_RCPT + while(recIter != to.end()) { + contentSS << "TO: " << *recIter << std::endl; + recIter++; + } + recIter = cc.begin(); + while(recIter != cc.end()) { + contentSS << "CC: " << *recIter << std::endl; + recIter++; + } + recIter = bcc.begin(); + while(recIter != bcc.end()) { + contentSS << "BCC: " << *recIter << std::endl; + recIter++; + } + + recIter = headers.begin(); + while(recIter != headers.end()) { + contentSS << *recIter << std::endl; + recIter++; + } + + if (subject.length() > 0) { + boost::replace_all(subject, "\n\r", " "); + boost::replace_all(subject, "\r\n", " "); + boost::replace_all(subject, "\n", " "); + boost::replace_all(subject, "\r", " "); + contentSS << "Subject: " << subject << "\n"; + } + + // content type is different when we have attachments + if (attachments.size() > 0) { + multipartSep = UUID::getUUID(); + boost::replace_all(multipartSep, "-", ""); + contentSS << "Content-Type: multipart/mixed; boundary=\"" << multipartSep << "\"\n"; + contentSS << "MIME-Version: 1.0\n"; + contentSS << "\n"; + contentSS << "--" << multipartSep << "\n"; + contentSS << "Content-Type: " << contentType << "\n"; + } else { + // when we have no attachment, respect user-defined or use text/plain + contentSS << "Content-Type: " << contentType << "\n"; + } + + contentSS << "\n"; + contentSS << req.content; + + std::list<Data>::iterator attIter = attachments.begin(); + while(attIter != attachments.end()) { + // only send valid attachments + if(!attIter->hasKey("filename") || !attIter->hasKey("mimetype") || !attIter->hasKey("data")) { + LOG(ERROR) << "Not sending attachment as filename, mimetype or data is missing: " << *attIter; + } else { + contentSS << "\n\n"; + contentSS << "--" << multipartSep << "\n"; + contentSS << "Content-Disposition: attachment; filename=\"" << attIter->compound["filename"].atom << "\""; + contentSS << "\n"; + + contentSS << "Content-Type: " << attIter->compound["mimetype"].atom << "; "; + contentSS << "name=\"" << attIter->compound["filename"].atom << "\""; + contentSS << "\n"; + + if (attIter->compound["data"].binary) { + contentSS << "Content-Transfer-Encoding: base64"; + contentSS << "\n\n"; + contentSS << attIter->compound["data"].binary->base64(); + } else { + contentSS << "Content-Transfer-Encoding: 7Bit"; + contentSS << "\n\n"; + contentSS << attIter->compound["data"].atom; + } + } + attIter++; + } + + ctx->content = contentSS.str(); + ctx->invoker = this; + + + // see http://curl.haxx.se/libcurl/c/smtp-tls.html + _curl = curl_easy_init(); + if(_curl) { + (curlError = curl_easy_setopt(_curl, CURLOPT_USERNAME, _username.c_str())) == CURLE_OK || + LOG(ERROR) << "Cannot set username: " << curl_easy_strerror(curlError); + (curlError = curl_easy_setopt(_curl, CURLOPT_PASSWORD, _password.c_str())) == CURLE_OK || + LOG(ERROR) << "Cannot set password: " << curl_easy_strerror(curlError); + (curlError = curl_easy_setopt(_curl, CURLOPT_URL, _server.c_str())) == CURLE_OK || + LOG(ERROR) << "Cannot set server string: " << curl_easy_strerror(curlError); + (curlError = curl_easy_setopt(_curl, CURLOPT_USE_SSL, (long)CURLUSESSL_ALL)) == CURLE_OK || + LOG(ERROR) << "Cannot use SSL: " << curl_easy_strerror(curlError); + + // this is needed, even if we have a callback function + recipients = curl_slist_append(recipients, to.begin()->c_str()); + (curlError = curl_easy_setopt(_curl, CURLOPT_MAIL_RCPT, recipients)) == CURLE_OK || + LOG(ERROR) << "Cannot set mail recipient: " << curl_easy_strerror(curlError); + + (curlError = curl_easy_setopt(_curl, CURLOPT_READFUNCTION, SMTPInvoker::readCurlData)) == CURLE_OK || + LOG(ERROR) << "Cannot register read function: " << curl_easy_strerror(curlError); + (curlError = curl_easy_setopt(_curl, CURLOPT_READDATA, ctx)) == CURLE_OK || + LOG(ERROR) << "Cannot register userdata for read function: " << curl_easy_strerror(curlError); + (curlError = curl_easy_setopt(_curl, CURLOPT_UPLOAD, 1L)) == CURLE_OK || + LOG(ERROR) << "Cannot set upload parameter: " << curl_easy_strerror(curlError); + +#if 1 + (curlError = curl_easy_setopt(_curl, CURLOPT_SSL_VERIFYPEER, 0L)) == CURLE_OK || + LOG(ERROR) << "Cannot unset verify peer with SSL: " << curl_easy_strerror(curlError); + (curlError = curl_easy_setopt(_curl, CURLOPT_SSL_VERIFYHOST, 0L)) == CURLE_OK || + LOG(ERROR) << "Cannot unset verify host with SSL: " << curl_easy_strerror(curlError); +#else + (curlError = curl_easy_setopt(_curl, CURLOPT_CAINFO, "/path/to/certificate.pem")) == CURLE_OK || + LOG(ERROR) << "Cannot set CA info path: " << curl_easy_strerror(curlError); +#endif + + if (from.length() > 0) { + (curlError = curl_easy_setopt(_curl, CURLOPT_MAIL_FROM, from.c_str())) == CURLE_OK || + LOG(ERROR) << "Cannot set from parameter: " << curl_easy_strerror(curlError); + } + + if (verbose) { + (curlError = curl_easy_setopt(_curl, CURLOPT_VERBOSE, 1L)) == CURLE_OK || + LOG(ERROR) << "Cannot set curl to verbose: " << curl_easy_strerror(curlError); + } + + CURLcode res = curl_easy_perform(_curl); + + /* Check for errors */ + if(res != CURLE_OK){ + LOG(ERROR) << "curl_easy_perform() failed: " << curl_easy_strerror(res); + returnErrorExecution("error.mail.send"); + } else { + returnErrorExecution("success.mail.send"); + } + /* Free the list of recipients */ + if (recipients) + curl_slist_free_all(recipients); + + /* Always cleanup */ + curl_easy_cleanup(_curl); + + } + + } +} + +void SMTPInvoker::cancel(const std::string sendId) { +} + +void SMTPInvoker::invoke(const InvokeRequest& req) { + Event::getParam(req.params, "username", _username); + Event::getParam(req.params, "password", _password); + Event::getParam(req.params, "server", _server); +} + +}
\ No newline at end of file diff --git a/src/uscxml/plugins/invoker/smtp/SMTPInvoker.h b/src/uscxml/plugins/invoker/smtp/SMTPInvoker.h new file mode 100644 index 0000000..f3876bd --- /dev/null +++ b/src/uscxml/plugins/invoker/smtp/SMTPInvoker.h @@ -0,0 +1,78 @@ +/** + * @file + * @author 2012-2013 Stefan Radomski (stefan.radomski@cs.tu-darmstadt.de) + * @copyright Simplified BSD + * + * @cond + * This program is free software: you can redistribute it and/or modify + * it under the terms of the FreeBSD license as published by the FreeBSD + * project. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the FreeBSD license along with this + * program. If not, see <http://www.opensource.org/licenses/bsd-license>. + * @endcond + */ + +#ifndef SMTPINVOKER_H_W09J90F0 +#define SMTPINVOKER_H_W09J90F0 + +#include <uscxml/Interpreter.h> + +#ifdef BUILD_AS_PLUGINS +#include "uscxml/plugins/Plugins.h" +#endif + +#include <curl/curl.h> + +namespace uscxml { + +class SMTPInvoker : public InvokerImpl { +public: + SMTPInvoker(); + virtual ~SMTPInvoker(); + virtual boost::shared_ptr<InvokerImpl> create(InterpreterImpl* interpreter); + + virtual std::set<std::string> getNames() { + std::set<std::string> names; + names.insert("smtp"); + names.insert("http://uscxml.tk.informatik.tu-darmstadt.de/#smtp"); + return names; + } + + virtual Data getDataModelVariables(); + virtual void send(const SendRequest& req); + virtual void cancel(const std::string sendId); + virtual void invoke(const InvokeRequest& req); + +protected: + + class SMTPContext { + public: + SMTPContext() : readPtr(0) {} + std::string content; + size_t readPtr; + SMTPInvoker* invoker; + }; + + CURL* _curl; + std::string _username; + std::string _password; + std::string _server; + + std::list<std::string> getAtoms(std::list<Data> list); + void getAttachments(std::list<Data> list, std::list<Data>& attachments); + static size_t readCurlData(void *ptr, size_t size, size_t nmemb, void *userdata); +}; + +#ifdef BUILD_AS_PLUGINS +PLUMA_INHERIT_PROVIDER(SMTPInvoker, InvokerImpl); +#endif + +} + + +#endif /* end of include guard: SMTPINVOKER_H_W09J90F0 */ |