public void ToXml(XDoc parent) { parent .Start("function") .Elem("name", Name); if (!string.IsNullOrEmpty(_access)) { parent.Elem("access", _access); } foreach (var parameter in _parameters) { parent .Start("param") .Attr("name", parameter.Key) .Attr("type", parameter.Value.Type); if (!string.IsNullOrEmpty(parameter.Value.Default)) { parent.Attr("default", parameter.Value.Default); } parent.End(); } parent .Start("return") .Attr("type", _type); if (_htmlBody != null) { parent.AddAll(_htmlBody); } else { parent.Value(_body); } parent .End() .End(); }
public ITestScriptService AddFunctionAsXml(XDoc functionDocument) { _manifest.AddAll(functionDocument); return(this); }
/// <summary> /// Create a new host with provided configuration and an Inversion of Control container. /// </summary> /// <remarks> /// The IoC container is also injected into default activator, so that <see cref="IDreamService"/> instances /// can be resolved from the container. The host configuration is provided to the container as a typed parameter. /// </remarks> /// <param name="config">Host configuration.</param> /// <param name="container">IoC Container.</param> public DreamHost(XDoc config, IContainer container) { if(config == null) { throw new ArgumentNullException("config"); } // read host settings string appDirectory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0].FullyQualifiedName); int limit = config["connect-limit"].AsInt ?? 0; int httpPort = config["http-port"].AsInt ?? DEFAULT_PORT; AuthenticationSchemes authenticationScheme = AuthenticationSchemes.Anonymous; string authShemes = config["authentication-shemes"].AsText; if(!String.IsNullOrEmpty(authShemes)) { try { authenticationScheme = (AuthenticationSchemes)Enum.Parse(typeof(AuthenticationSchemes), authShemes, true); } catch(Exception e) { _log.Warn(String.Format("invalid authetication scheme specified :{0}", authShemes), e); } } // get the authtoken for whitelisting dream.in.* query args _dreamInParamAuthtoken = config["dream.in.authtoken"].AsText; if(!string.IsNullOrEmpty(_dreamInParamAuthtoken)) { _log.Debug("Host is configured in dream.in param authorizing mode"); } // read ip-addresses var addresses = new List<string>(); foreach(XDoc ip in config["host|ip"]) { addresses.Add(ip.AsText); } if(addresses.Count == 0) { // if no addresses were supplied listen to all addresses.Add("*:" + httpPort); } // use default servername XUri publicUri = config["uri.public"].AsUri; if(publicUri == null) { // backwards compatibility publicUri = config["server-name"].AsUri; if(publicUri == null) { foreach(IPAddress addr in Dns.GetHostAddresses(Dns.GetHostName())) { if(addr.AddressFamily == AddressFamily.InterNetwork) { XUri.TryParse("http://" + addr, out publicUri); } } if(publicUri == null) { // failed to get an address out of dns, fall back to localhost XUri.TryParse("http://localhost", out publicUri); } } publicUri = publicUri.AtPath(config["server-path"].AsText ?? config["path-prefix"].AsText ?? string.Empty); } // create environment and initialize it _env = new DreamHostService(container); try { // initialize environment string apikey = config["apikey"].AsText ?? StringUtil.CreateAlphaNumericKey(32); XDoc serviceConfig = new XDoc("config"); var storageType = config["storage/@type"].AsText ?? "local"; if("s3".EqualsInvariant(storageType)) { serviceConfig.Add(config["storage"]); } else { serviceConfig.Elem("storage-dir", config["storage-dir"].AsText ?? config["service-dir"].AsText ?? appDirectory); } serviceConfig.Elem("apikey", apikey); serviceConfig.Elem("uri.public", publicUri); serviceConfig.Elem("connect-limit", limit); serviceConfig.Elem("guid", config["guid"].AsText); serviceConfig.AddAll(config["components"]); var memorize = config["memorize-aliases"]; if(!memorize.IsEmpty) { serviceConfig.Elem("memorize-aliases", memorize.AsBool); } _env.Initialize(serviceConfig); // initialize host plug _host = _env.Self.With("apikey", apikey); // load assemblies in 'services' folder string servicesFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "services"); if(Directory.Exists(servicesFolder)) { // Note (arnec): Deprecated, but the suggested alternative really doesn't apply since we don't want to // load services into a separate appdomain. #pragma warning disable 618,612 AppDomain.CurrentDomain.AppendPrivatePath("services"); #pragma warning restore 618,612 foreach(string file in Directory.GetFiles(servicesFolder, "*.dll")) { // register assembly blueprints DreamMessage response = _host.At("load").With("name", Path.GetFileNameWithoutExtension(file)).Post(new Result<DreamMessage>(TimeSpan.MaxValue)).Wait(); if(!response.IsSuccessful) { _log.WarnFormat("DreamHost: ERROR: assembly '{0}' failed to load", file); } } } // add acccess-points AddListener(new XUri(String.Format("http://{0}:{1}/", "localhost", httpPort)), authenticationScheme); // check if user prescribed a set of IP addresses to use if(addresses != null) { // listen to custom addresses (don't use the supplied port info, we expect that to be part of the address) foreach(string address in addresses) { if(!StringUtil.EqualsInvariantIgnoreCase(address, "localhost")) { AddListener(new XUri(String.Format("http://{0}/", address)), authenticationScheme); } } } else { // add listeners for all known IP addresses foreach(IPAddress address in Dns.GetHostAddresses(Dns.GetHostName())) { XUri uri = MakeUri(address, httpPort); if(uri != null) { AddListener(uri, authenticationScheme); try { foreach(string alias in Dns.GetHostEntry(address).Aliases) { AddListener(new XUri(String.Format("http://{0}:{1}/", alias, httpPort)), authenticationScheme); } } catch { } } } } } catch(Exception e) { if((e is HttpListenerException) && e.Message.EqualsInvariant("Access is denied")) { _log.ErrorExceptionMethodCall(e, "ctor", "insufficient privileges to create HttpListener, make sure the application runs with Administrator rights"); } else { _log.ErrorExceptionMethodCall(e, "ctor"); } try { _env.Deinitialize(); } catch { } throw; } }
public void ToXml(XDoc parent) { parent .Start("function") .Elem("name", Name); if(!string.IsNullOrEmpty(_access)) { parent.Elem("access", _access); } foreach(var parameter in _parameters) { parent .Start("param") .Attr("name", parameter.Key) .Attr("type", parameter.Value.Type); if(!string.IsNullOrEmpty(parameter.Value.Default)) { parent.Attr("default", parameter.Value.Default); } parent.End(); } parent .Start("return") .Attr("type", _type); if(_htmlBody != null) { parent.AddAll(_htmlBody); } else { parent.Value(_body); } parent .End() .End(); }
private Yield SendEmail(NotificationUpdateRecord updateRecord, Result result) { bool userChanged = false; Plug deki = _deki.With("apikey", _apikey).WithCookieJar(Cookies); _log.DebugFormat("trying to dispatch email to user {0} for wiki '{1}'", updateRecord.UserId, updateRecord.WikiId); bool createUser = false; UserInfo userInfo = _subscriptions.GetUser(updateRecord.WikiId, updateRecord.UserId, false); if(userInfo == null) { createUser = true; _log.DebugFormat("user is gone from subscriptions. Trying to re-fetch", updateRecord.UserId, updateRecord.WikiId); } if(userInfo == null || !userInfo.IsValidated) { // need to refetch user info to make sure we have DreamMessage userMsg = null; yield return deki.At("users", updateRecord.UserId.ToString()).WithHeader("X-Deki-Site", "id=" + updateRecord.WikiId) .Get(new Result<DreamMessage>()) .Set(x => userMsg = x); if(!userMsg.IsSuccessful) { _log.DebugFormat("unable to fetch user {0}, skipping delivery: {1}", updateRecord.UserId, userMsg.Status); result.Return(); yield break; } var userDoc = userMsg.ToDocument(); try { userInfo = GetUserInfo(userDoc, updateRecord.WikiId, createUser); } catch(UserException e) { _log.DebugFormat("unable to re-validate user {0}, skipping delivery: {1}", updateRecord.UserId, e.Message); result.Return(); yield break; } userInfo.Save(); } SiteInfo siteInfo = _subscriptions.GetSiteInfo(updateRecord.WikiId); if(!siteInfo.IsValidated) { // lazy loading site information Result<DreamMessage> siteResult; yield return siteResult = deki.At("site", "settings").WithHeader("X-Deki-Site", "id=" + updateRecord.WikiId).GetAsync(); DreamMessage site = siteResult.Value; if(!site.IsSuccessful) { _log.WarnFormat("unable to fetch site data for deki '{0}', skipping delivery: {1}", updateRecord.WikiId, site.Status); result.Return(); yield break; } XDoc siteDoc = site.ToDocument(); siteInfo.Sitename = siteDoc["ui/sitename"].AsText; siteInfo.EmailFromAddress = siteDoc["page-subscription/from-address"].AsText; siteInfo.EmailFormat = siteDoc["page-subscription/email-format"].AsText; if(string.IsNullOrEmpty(siteInfo.EmailFromAddress)) { siteInfo.EmailFromAddress = siteDoc["admin/email"].AsText; } siteInfo.Culture = CultureUtil.GetNonNeutralCulture(siteDoc["ui/language"].AsText) ?? CultureInfo.GetCultureInfo("en-us"); if(!siteInfo.IsValidated) { _log.WarnFormat("unable to get required data from site settings, cannot send email"); if(string.IsNullOrEmpty(siteInfo.Sitename)) { _log.WarnFormat("missing ui/sitename"); } if(string.IsNullOrEmpty(siteInfo.EmailFromAddress)) { _log.WarnFormat("missing page-subscription/from-address"); } result.Return(); yield break; } } CultureInfo culture = CultureUtil.GetNonNeutralCulture(userInfo.Culture, siteInfo.Culture); string subject = string.Format("[{0}] {1}", siteInfo.Sitename, _resourceManager.GetString("Notification.Page.email-subject", culture, "Site Modified")); XDoc email = new XDoc("email") .Attr("configuration", siteInfo.WikiId) .Elem("to", userInfo.Email) .Elem("from", siteInfo.EmailFromAddress) .Elem("subject", subject) .Start("pages"); string header = _resourceManager.GetString("Notification.Page.email-header", culture, "The following pages have changed:"); StringBuilder plainBody = new StringBuilder(); plainBody.AppendFormat("{0}\r\n\r\n", header); XDoc htmlBody = new XDoc("body") .Attr("html", true) .Elem("h2", header); foreach(Tuplet<uint, DateTime, bool> record in updateRecord.Pages) { // TODO (arnec): Need to revalidate that the user is still allowed to see that page // TODO (arnec): Should check that the user is still subscribed to this page uint pageId = record.Item1; email.Elem("pageid", pageId); Result<PageChangeData> dataResult; yield return dataResult = Coroutine.Invoke(_cache.GetPageData, pageId, userInfo.WikiId, record.Item2, culture, userInfo.Timezone, new Result<PageChangeData>()); PageChangeData data = dataResult.Value; if(data == null) { _log.WarnFormat("Unable to fetch page change data for page {0}", pageId); continue; } htmlBody.AddAll(data.HtmlBody.Elements); plainBody.Append(data.PlainTextBody); if(!record.Item3) { continue; } userInfo.RemoveResource(pageId); userChanged = true; } email.End(); if(!StringUtil.EqualsInvariantIgnoreCase(siteInfo.EmailFormat, "html")) { email.Elem("body", plainBody.ToString()); } if(!StringUtil.EqualsInvariantIgnoreCase(siteInfo.EmailFormat, "plaintext")) { email.Add(htmlBody); } _log.DebugFormat("dispatching email for user '{0}'", userInfo.Id); yield return _emailer.WithCookieJar(Cookies).PostAsync(email).Catch(); if(userChanged) { userInfo.Save(); } result.Return(); yield break; }
/// <summary> /// Create a new host with provided configuration and an Inversion of Control container. /// </summary> /// <remarks> /// The IoC container is also injected into default activator, so that <see cref="IDreamService"/> instances /// can be resolved from the container. The host configuration is provided to the container as a typed parameter. /// </remarks> /// <param name="config">Host configuration.</param> /// <param name="container">IoC Container.</param> public DreamHost(XDoc config, IContainer container) { if (config == null) { throw new ArgumentNullException("config"); } // read host settings string appDirectory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0].FullyQualifiedName); int limit = config["connect-limit"].AsInt ?? 0; int httpPort = config["http-port"].AsInt ?? DEFAULT_PORT; AuthenticationSchemes authenticationScheme = AuthenticationSchemes.Anonymous; string authShemes = config["authentication-shemes"].AsText; if (!String.IsNullOrEmpty(authShemes)) { try { authenticationScheme = (AuthenticationSchemes)Enum.Parse(typeof(AuthenticationSchemes), authShemes, true); } catch (Exception e) { _log.Warn(String.Format("invalid authetication scheme specified :{0}", authShemes), e); } } // get the authtoken for whitelisting dream.in.* query args _dreamInParamAuthtoken = config["dream.in.authtoken"].AsText; if (!string.IsNullOrEmpty(_dreamInParamAuthtoken)) { _log.Debug("Host is configured in dream.in param authorizing mode"); } // read ip-addresses var addresses = new List <string>(); foreach (XDoc ip in config["host|ip"]) { addresses.Add(ip.AsText); } if (addresses.Count == 0) { // if no addresses were supplied listen to all addresses.Add("*:" + httpPort); } // use default servername XUri publicUri = config["uri.public"].AsUri; if (publicUri == null) { // backwards compatibility publicUri = config["server-name"].AsUri; if (publicUri == null) { foreach (IPAddress addr in Dns.GetHostAddresses(Dns.GetHostName())) { if (addr.AddressFamily == AddressFamily.InterNetwork) { XUri.TryParse("http://" + addr, out publicUri); } } if (publicUri == null) { // failed to get an address out of dns, fall back to localhost XUri.TryParse("http://localhost", out publicUri); } } publicUri = publicUri.AtPath(config["server-path"].AsText ?? config["path-prefix"].AsText ?? string.Empty); } // create environment and initialize it _env = new DreamHostService(container); try { // initialize environment string apikey = config["apikey"].AsText ?? StringUtil.CreateAlphaNumericKey(32); XDoc serviceConfig = new XDoc("config"); var storageType = config["storage/@type"].AsText ?? "local"; if ("s3".EqualsInvariant(storageType)) { serviceConfig.Add(config["storage"]); } else { serviceConfig.Elem("storage-dir", config["storage-dir"].AsText ?? config["service-dir"].AsText ?? appDirectory); } serviceConfig.Elem("apikey", apikey); serviceConfig.Elem("uri.public", publicUri); serviceConfig.Elem("connect-limit", limit); serviceConfig.Elem("guid", config["guid"].AsText); serviceConfig.AddAll(config["components"]); var memorize = config["memorize-aliases"]; if (!memorize.IsEmpty) { serviceConfig.Elem("memorize-aliases", memorize.AsBool); } _env.Initialize(serviceConfig); // initialize host plug _host = _env.Self.With("apikey", apikey); // load assemblies in 'services' folder string servicesFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "services"); if (Directory.Exists(servicesFolder)) { // Note (arnec): Deprecated, but the suggested alternative really doesn't apply since we don't want to // load services into a separate appdomain. #pragma warning disable 618,612 AppDomain.CurrentDomain.AppendPrivatePath("services"); #pragma warning restore 618,612 foreach (string file in Directory.GetFiles(servicesFolder, "*.dll")) { // register assembly blueprints DreamMessage response = _host.At("load").With("name", Path.GetFileNameWithoutExtension(file)).Post(new Result <DreamMessage>(TimeSpan.MaxValue)).Wait(); if (!response.IsSuccessful) { _log.WarnFormat("DreamHost: ERROR: assembly '{0}' failed to load", file); } } } // add acccess-points AddListener(new XUri(String.Format("http://{0}:{1}/", "localhost", httpPort)), authenticationScheme); // check if user prescribed a set of IP addresses to use if (addresses != null) { // listen to custom addresses (don't use the supplied port info, we expect that to be part of the address) foreach (string address in addresses) { if (!StringUtil.EqualsInvariantIgnoreCase(address, "localhost")) { AddListener(new XUri(String.Format("http://{0}/", address)), authenticationScheme); } } } else { // add listeners for all known IP addresses foreach (IPAddress address in Dns.GetHostAddresses(Dns.GetHostName())) { XUri uri = MakeUri(address, httpPort); if (uri != null) { AddListener(uri, authenticationScheme); try { foreach (string alias in Dns.GetHostEntry(address).Aliases) { AddListener(new XUri(String.Format("http://{0}:{1}/", alias, httpPort)), authenticationScheme); } } catch { } } } } } catch (Exception e) { if ((e is HttpListenerException) && e.Message.EqualsInvariant("Access is denied")) { _log.ErrorExceptionMethodCall(e, "ctor", "insufficient privileges to create HttpListener, make sure the application runs with Administrator rights"); } else { _log.ErrorExceptionMethodCall(e, "ctor"); } try { _env.Deinitialize(); } catch { } throw; } }
//--- Class Methods --- public static XDoc Include(string path, string section, bool showTitle, int? adjustHeading, DekiScriptLiteral args, int revision, bool followRedirects, bool returnEmptyDocIfNotFound, bool createAnchor) { var resources = DekiContext.Current.Resources; // retrieve the page requested Title titleToInclude = Title.FromUriPath(path); PageBE pageToInclude = PageBL.GetPageByTitle(titleToInclude); if(followRedirects) { pageToInclude = PageBL.ResolveRedirects(pageToInclude); } // If the page was not found, create a link to the page placeholder if(pageToInclude.ID == 0) { return returnEmptyDocIfNotFound ? XDoc.Empty : new XDoc("html").Start("body").Start("a").Attr("href", path).Value(titleToInclude.AsUserFriendlyName()).End().End(); } if(!PermissionsBL.IsUserAllowed(DekiContext.Current.User, pageToInclude, Permissions.READ)) { return new XDoc("html").Start("body").Value(resources.Localize(DekiResources.RESTRICT_MESSAGE())).End(); } // load old revision of page if(revision != 0) { PageBL.ResolvePageRev(pageToInclude, revision.ToString()); } // parse the page ParserResult result = Parse(pageToInclude, pageToInclude.ContentType, pageToInclude.Language, pageToInclude.GetText(DbUtils.CurrentSession), ParserMode.VIEW, true, -1, args, null); // if requested, extract the specified section XDoc doc; if(section != null) { XmlNode startNode, endNode; FindSection(result.MainBody, section, out startNode, out endNode); XDoc body = ExtractSection(false, startNode, endNode); // If the section was not found, create a link to the page whose section is missing if(null == startNode) { if(returnEmptyDocIfNotFound) { return XDoc.Empty; } body = body.Start("a").Attr("href", path).Attr("rel", "internal").Value(titleToInclude.AsUserFriendlyName()).End(); } else if(adjustHeading.HasValue) { // header needs to be adjusted int current; if(int.TryParse(startNode.LocalName.Substring(1), out current)) { if(showTitle) { body.AddNodesInFront(new XDoc("body").Add(result.MainBody[startNode])); } RelabelHeadings(body, adjustHeading.Value - current); } } doc = new XDoc("html").Add(result.Head).Add(body).Add(result.Tail); } else { doc = new XDoc("html").Add(result.Head); // check if header needs to be adjusted if(adjustHeading.HasValue) { if(showTitle) { result.MainBody.AddNodesInFront(new XDoc("body").Elem("h1", pageToInclude.CustomTitle ?? pageToInclude.Title.AsUserFriendlyName())); } RelabelHeadings(result.MainBody, adjustHeading.Value - 1); } // add optional anchor if(createAnchor) { result.MainBody.AddNodesInFront(new XDoc("body").Start("span").Attr("pagelink", DekiContext.Current.UiUri.AtPath(pageToInclude.Title.AsUiUriPath())).Attr("id", "s" + pageToInclude.ID).End()); } doc.AddAll(result.Bodies).Add(result.Tail); } // replace all <span id="page.toc" /> place holders if(!pageToInclude.Title.IsTemplate) { var toc = ProcessPageHeadings(doc); XDoc tocDiv = null; foreach(XDoc pageToc in doc["body//span[@id='page.toc']"]) { if(tocDiv == null) { tocDiv = new XDoc("div").Attr("class", "wiki-toc").AddNodes(toc); } pageToc.Replace(TruncateTocDepth(tocDiv, pageToc["@depth"].AsInt)); } } return doc; }
private Yield SendEmail(DreamContext context, DreamMessage request, Result <DreamMessage> result) { var updateRecord = NotificationUpdateRecord.FromDocument(request.ToDocument()); _log.DebugFormat("trying to dispatch email to user {0} for wiki '{1}'", updateRecord.UserId, updateRecord.WikiId); var instance = GetInstanceInfo(updateRecord.WikiId); if (!instance.IsValid) { _log.WarnFormat("unable to get required data from site settings, cannot send email. Missing either ui/sitename or page-subscription/from-address "); result.Return(DreamMessage.Ok()); yield break; } var userInfo = instance.GetUserInfo(updateRecord.UserId); if (!userInfo.IsValid) { // need to refetch user info DreamMessage userMsg = null; yield return(_deki.At("users", updateRecord.UserId.ToString()) .With("apikey", _apikey) .WithCookieJar(Cookies) .WithHeader("X-Deki-Site", "id=" + updateRecord.WikiId) .Get(new Result <DreamMessage>()) .Set(x => userMsg = x)); if (!userMsg.IsSuccessful) { _log.DebugFormat("unable to fetch user {0}, skipping delivery: {1}", updateRecord.UserId, userMsg.Status); result.Return(DreamMessage.Ok()); yield break; } var userDoc = userMsg.ToDocument(); try { PopulateUser(userInfo, userDoc); } catch (UserException e) { _log.DebugFormat("unable to populate user {0}, skipping delivery: {1}", updateRecord.UserId, e.Message); result.Return(DreamMessage.Ok()); yield break; } } var culture = userInfo.Culture.GetNonNeutralCulture(instance.Culture); var subject = string.Format("[{0}] {1}", instance.Sitename, _resourceManager.GetString("Notification.Page.email-subject", culture, "Site Modified")); var emailAddress = (!instance.UseShortEmailAddress && !string.IsNullOrEmpty(userInfo.Username)) ? new MailAddress(userInfo.Email, userInfo.Username).ToString() : userInfo.Email; var email = new XDoc("email") .Attr("configuration", instance.WikiId) .Elem("to", emailAddress) .Elem("from", instance.EmailFromAddress) .Elem("subject", subject) .Start("pages"); var header = _resourceManager.GetString("Notification.Page.email-header", culture, "The following pages have changed:"); var plainBody = new StringBuilder(); plainBody.AppendFormat("{0}\r\n\r\n", header); var htmlBody = new XDoc("body") .Attr("html", true) .Elem("h2", header); foreach (Tuplet <uint, DateTime> record in updateRecord.Pages) { var pageId = record.Item1; email.Elem("pageid", pageId); PageChangeData data = null; var timezone = userInfo.Timezone.IfNullOrEmpty(instance.Timezone); yield return(Coroutine.Invoke(_cache.GetPageData, pageId, instance.WikiId, record.Item2, culture, timezone, new Result <PageChangeData>()).Set(x => data = x)); if (data == null) { _log.WarnFormat("Unable to fetch page change data for page {0}", pageId); continue; } htmlBody.AddAll(data.HtmlBody.Elements); plainBody.Append(data.PlainTextBody); } email.End(); if (!instance.EmailFormat.EqualsInvariantIgnoreCase("html")) { email.Elem("body", plainBody.ToString()); } if (!instance.EmailFormat.EqualsInvariantIgnoreCase("plaintext")) { email.Add(htmlBody); } _log.DebugFormat("dispatching email for user '{0}'", userInfo.Id); yield return(_emailer.WithCookieJar(Cookies).PostAsync(email).Catch()); result.Return(DreamMessage.Ok()); yield break; }
private Yield SendEmail(DreamContext context, DreamMessage request, Result<DreamMessage> result) { var updateRecord = NotificationUpdateRecord.FromDocument(request.ToDocument()); _log.DebugFormat("trying to dispatch email to user {0} for wiki '{1}'", updateRecord.UserId, updateRecord.WikiId); var instance = GetInstanceInfo(updateRecord.WikiId); if(!instance.IsValid) { _log.WarnFormat("unable to get required data from site settings, cannot send email. Missing either ui/sitename or page-subscription/from-address "); result.Return(DreamMessage.Ok()); yield break; } var userInfo = instance.GetUserInfo(updateRecord.UserId); if(!userInfo.IsValid) { // need to refetch user info DreamMessage userMsg = null; yield return _deki.At("users", updateRecord.UserId.ToString()) .With("apikey", _apikey) .WithCookieJar(Cookies) .WithHeader("X-Deki-Site", "id=" + updateRecord.WikiId) .Get(new Result<DreamMessage>()) .Set(x => userMsg = x); if(!userMsg.IsSuccessful) { _log.DebugFormat("unable to fetch user {0}, skipping delivery: {1}", updateRecord.UserId, userMsg.Status); result.Return(DreamMessage.Ok()); yield break; } var userDoc = userMsg.ToDocument(); try { PopulateUser(userInfo, userDoc); } catch(UserException e) { _log.DebugFormat("unable to populate user {0}, skipping delivery: {1}", updateRecord.UserId, e.Message); result.Return(DreamMessage.Ok()); yield break; } } var culture = userInfo.Culture.GetNonNeutralCulture(instance.Culture); var subject = string.Format("[{0}] {1}", instance.Sitename, _resourceManager.GetString("Notification.Page.email-subject", culture, "Site Modified")); var emailAddress = (!instance.UseShortEmailAddress && !string.IsNullOrEmpty(userInfo.Username)) ? new MailAddress(userInfo.Email, userInfo.Username).ToString() : userInfo.Email; var email = new XDoc("email") .Attr("configuration", instance.WikiId) .Elem("to", emailAddress) .Elem("from", instance.EmailFromAddress) .Elem("subject", subject) .Start("pages"); var header = _resourceManager.GetString("Notification.Page.email-header", culture, "The following pages have changed:"); var plainBody = new StringBuilder(); plainBody.AppendFormat("{0}\r\n\r\n", header); var htmlBody = new XDoc("body") .Attr("html", true) .Elem("h2", header); foreach(Tuplet<uint, DateTime> record in updateRecord.Pages) { var pageId = record.Item1; email.Elem("pageid", pageId); PageChangeData data = null; var timezone = userInfo.Timezone.IfNullOrEmpty(instance.Timezone); yield return Coroutine.Invoke(_cache.GetPageData, pageId, instance.WikiId, record.Item2, culture, timezone, new Result<PageChangeData>()).Set(x => data = x); if(data == null) { _log.WarnFormat("Unable to fetch page change data for page {0}", pageId); continue; } htmlBody.AddAll(data.HtmlBody.Elements); plainBody.Append(data.PlainTextBody); } email.End(); if(!instance.EmailFormat.EqualsInvariantIgnoreCase("html")) { email.Elem("body", plainBody.ToString()); } if(!instance.EmailFormat.EqualsInvariantIgnoreCase("plaintext")) { email.Add(htmlBody); } _log.DebugFormat("dispatching email for user '{0}'", userInfo.Id); yield return _emailer.WithCookieJar(Cookies).PostAsync(email).Catch(); result.Return(DreamMessage.Ok()); yield break; }