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();
 }
Esempio n. 2
0
 public ITestScriptService AddFunctionAsXml(XDoc functionDocument)
 {
     _manifest.AddAll(functionDocument);
     return(this);
 }
Esempio n. 3
0
        /// <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;
            }
        }
Esempio n. 4
0
 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;
        }
Esempio n. 6
0
        /// <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;
            }
        }
Esempio n. 7
0
        //--- 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;
        }