private void ValidateAccounts() { const string SOURCE = CLASS_NAME + "ValidateAccounts"; try { //Session.Accounts doesn't update immediately when you add or remove an account //try RDOSession stores if available if (ValidateAccountsRdo()) return; var accounts = Session.Accounts; Logger.Info(SOURCE, string.Format( "comparing {0} OL accounts against {1} ECS accounts", accounts.Count, Accounts.Count)); //check for deletes first var keys = Accounts.Keys; foreach (var key in keys .TakeWhile(key => accounts.Cast<Outlook.Account>() .All(olAcct => olAcct.SmtpAddress != key))) { Logger.Info(SOURCE, string.Format( "removing account with key {0}", key)); Accounts.Remove(key); } //now check for new OL accounts foreach (Outlook.Account olAcct in accounts) { if (Accounts.ContainsKey(olAcct.SmtpAddress)) continue; var userName = olAcct.UserName; //OL 2K7 doesn't expose CurrentUser if (AppVersion > 12) userName = olAcct.CurrentUser.Name; var account = new Account { UserName = userName, SMTPAddress = olAcct.SmtpAddress, DefaultConfiguration = 0, Configurations = new Dictionary<int, EcsConfiguration> { { 0, new EcsConfiguration { Key = 0, Description = Resources.config_public_server_description, Password = string.Empty, Server = PUBLIC_SERVER, Port = Resources.default_port, DefaultOn = false, Encrypt = false, NoPlaceholder = false, AllowForwarding = false } } } }; GetAccountInfoRDO(olAcct,ref account); Accounts.Add(account.SMTPAddress, account); } } catch (Exception ex) { Logger.Error(SOURCE, ex.ToString()); } }
internal static RegistrationState CheckSenderRegistration(string sender,Account acct, out string error) { const string SOURCE = "CheckSenderRegistration"; var registered = false; var hasExpired = true; RegistrationCheck check; if (RegistrationChecks.TryGetValue(sender, out check)) { hasExpired = (check.LastCheck.Date != DateTime.Today.Date); registered = check.Registered == RegistrationState.Registered; } if (registered || !hasExpired) { error = string.Empty; return check.Registered; } //if (!registered && hasExpired) Logger.Info(SOURCE, string.Format( "checking registration for {0}", sender)); //var acct = ValidAccount; if (acct == null) { Logger.Info(SOURCE, string.Format( "no valid accounts, unable to submit 'CheckRegistered' request for {0}", sender)); error = "no valid accounts"; return RegistrationState.Unknown; } //default is NotRegistered check.Registered = RegistrationState.NotRegistered; check.Registered = ContentHandler.CheckRegistered(acct.SMTPAddress, acct.Configurations[0], sender, out error); //if (string.IsNullOrEmpty(response)) //{ // check.Registered = RegistrationState.Unknown; //} //else //{ // bool answer; // if (bool.TryParse(response, out answer)) // { // check.Registered = answer // ? RegistrationState.Registered // : RegistrationState.NotRegistered; // } // else if (response == "invalid credentials") // { // check.Registered = RegistrationState.BadCredentials; // } // else if (response == ) // { // check.Registered = RegistrationState.ServerError; // } //} //registered = check.Registered == RegistrationState.Registered; //store the result check.LastCheck = DateTime.Now; if (RegistrationChecks.ContainsKey(sender)) { RegistrationChecks[sender] = check; } else { RegistrationChecks.Add(sender, check); } return check.Registered; }
private List<Attachment> FetchParentAttachContent(MailItem parent, IList<string> pointers, string key, Account account, EcsConfiguration configuration, string senderAddress, string serverName, string serverPort, string encryptKey, string encryptKey2) { const string SOURCE = CLASS_NAME + "FetchParentAttachContent"; var attachList = new List<Attachment>(); try { var attachments = parent.Attachments; if (pointers.Count > 1 && attachments.Count > 0) { //we need the dynamic content for each attachment var parentPath = Path.Combine(Path.GetTempPath(), "ChiaraMail", key); MAPIUtils mapiUtils; try { mapiUtils = RedemptionLoader.new_MAPIUtils(); } catch (Exception ex) { Logger.Warning(SOURCE, string.Format( "unable to fetch ECS-enabled content for attachments on {0} - error loading MAPIUtils: {1}", parent.Subject, ex.Message)); return null; } for (var i = 1; i < attachments.Count + 1; i++) { var attach = attachments[i]; var cmAttach = new Attachment { Index = i, Pointer = pointers[i], Name = attach.DisplayName, Type = (int)attach.Type }; //only check ByValue - embedded message isn't stored on server if (attach.Type == OlAttachmentType.olByValue) { //check for content-id on hidden attachment var contentId = string.Empty; try { var prop = mapiUtils.HrGetOneProp(attach, (int) MAPITags.PR_ATTACHMENT_HIDDEN); if (prop != null && Convert.ToBoolean(prop)) { prop = mapiUtils.HrGetOneProp(attach, PR_ATTACH_CONTENT_ID); if (prop != null) contentId = Convert.ToString(prop); } } catch { contentId = ""; } string attachPath; if (!string.IsNullOrEmpty(contentId)) { attachPath = Path.Combine(parentPath, contentId); cmAttach.ContentId = contentId; } else { attachPath = Path.Combine(parentPath, Convert.ToString(i), attach.DisplayName); } if (File.Exists(attachPath)) { //load the bytes cmAttach.Content = File.ReadAllBytes(attachPath); } else { //fetch it string error; string content; ContentHandler.FetchContent(account.SMTPAddress, configuration, senderAddress, pointers[i], serverName, serverPort, true, out content, out error); if (!string.IsNullOrEmpty(error) && !error.Equals("Success",StringComparison.CurrentCultureIgnoreCase)) { Logger.Warning(SOURCE, string.Format( "failed to retrieve content for attachment {0} on item from sender {1}", i, senderAddress)); continue; } cmAttach.Content = ContentHandler.GetAttachBytes( content, encryptKey, encryptKey2); } } attachList.Add(cmAttach); } } } catch (Exception ex) { Logger.Error(SOURCE, ex.ToString()); } return attachList; }
private void GetAccountInfoRDO(Outlook.Account olAccount, ref Account account) { var rdoSession = RedemptionLoader.new_RDOSession(); rdoSession.MAPIOBJECT = Session.MAPIOBJECT; try { var olStore = olAccount.DeliveryStore; if (olStore == null) return; RDOStore rdoStore = rdoSession.GetRDOObjectFromOutlookObject(olStore, true); if (rdoStore == null) return; var acct = rdoStore.StoreAccount; if(acct == null) return; switch (acct.AccountType) { case rdoAccountType.atExchange: var exAcct = acct as RDOExchangeAccount; if (exAcct == null) return; var user = exAcct.User; //add all SMTP proxies List<string> proxies = null; try { var prop = user.Fields[(int)MAPITags.PR_EMS_AB_PROXY_ADDRESSES]; if (prop != null) { var addresses = prop as object[]; if (addresses != null) { proxies = addresses.Select(Convert.ToString).ToList(); } } } catch { proxies = null; } List<string> smtpProxies = null; if (proxies != null) { smtpProxies = (from proxy in proxies where proxy.StartsWith("SMTP:", StringComparison.InvariantCultureIgnoreCase) select proxy.Replace("SMTP:", "").Replace("smtp:", "")).ToList(); } AddAccountProxies(user.SMTPAddress,smtpProxies); break; case rdoAccountType.atIMAP: var imap = (RDOIMAPAccount) acct; //account.Password = imap.IMAP_Password; account.Host = imap.IMAP_Server; account.Port = Convert.ToString(imap.IMAP_Port); account.Protocol = "IMAP"; account.LoginName = imap.IMAP_UserName; break; case rdoAccountType.atPOP3: var pop3 = (RDOPOP3Account) acct; //account.Password = pop3.POP3_Password; account.Host = pop3.POP3_Server; account.Port = Convert.ToString(pop3.POP3_Port); account.Protocol = "POP3"; account.LoginName = pop3.POP3_UserName; break; case rdoAccountType.atEAS: var eas = (RDOEASAccount) acct; account.Host = eas.Server; account.Port = ""; account.Protocol = "EAS"; //account.Password = eas.Password; account.UserName = eas.UserName; break; case rdoAccountType.atHTTP: var http = (RDOHTTPAccount) acct; account.Host = http.Server; account.Port = ""; account.Protocol = "HTTP"; account.UserName = http.UserName; break; default: return; } } catch(Exception ex) { Logger.Error("GetAccountInfoRDO",string.Format( "error for olAccount {0}: {1}", olAccount.DisplayName, ex)); } }
internal static void GetFile(string pointer, string name, int index, string recordKey, Account account, EcsConfiguration configuration, string senderAddress, string serverName, string serverPort, string encryptKey, string encryptKey2, string userAgent, out string path, out string hash) { const string SOURCE = CLASS_NAME + "GetFile"; path = ""; hash = ""; if (string.IsNullOrEmpty(recordKey)) return; try { path = GetFilePath(recordKey, index, name); if (!File.Exists(path)) { //fetch the content string content; string error; ContentHandler.FetchContent(account.SMTPAddress, configuration, senderAddress, pointer, serverName, serverPort, true, out content, out error); if (string.IsNullOrEmpty(content)) { Logger.Warning(SOURCE, string.Format( "failed to retrieve content for {0} using pointer {1} from {2}: {3}", name, pointer, senderAddress, error)); return; } ContentHandler.SaveAttachment( content, encryptKey, encryptKey2, userAgent, path); } //return the hash byte[] buf = File.ReadAllBytes(path); hash = Cryptography.GetHash(buf); } catch (Exception ex) { Logger.Error(SOURCE, ex.ToString()); } }
public static void AssignHeaders(Outlook.MailItem item, Account account, List<string> pointers, string encryptKey, bool encrypted) { const string SOURCE = CLASS_NAME + "AssignHeaders"; try { var accessor = item.PropertyAccessor; //set each property individually //accessor fails to set the content header if there are too many pointer var pointerString = String.Join(" ", pointers.ToArray()); accessor.SetProperty( ThisAddIn.MAIL_HEADER_GUID + Resources.content_header, pointerString); var config = account.Configurations[account.DefaultConfiguration]; accessor.SetProperty( ThisAddIn.MAIL_HEADER_GUID + Resources.server_header, config.Server); accessor.SetProperty( ThisAddIn.MAIL_HEADER_GUID + Resources.port_header, config.Port); if (encrypted && !String.IsNullOrEmpty(encryptKey)) { //only write to new encrypt key header accessor.SetProperty(ThisAddIn.MAIL_HEADER_GUID + Resources.encrypt_key_header2, encryptKey); } accessor.SetProperty( ThisAddIn.MAIL_HEADER_GUID + Resources.user_agent_header, Resources.label_help_group + " " + AssemblyFullVersion); } catch (Exception ex) { Logger.Error(SOURCE, ex.ToString()); } }
internal static string FetchEmbeddedFileImages(string content, List<Match> matches, Dictionary<string,string> pointerMap, string baseUrl, Account account, EcsConfiguration configuration, string senderAddress, string serverName, string serverPort, string encryptKey2, string userAgent, ref List<string> embeddedFileNames) { //extract the src paths, if any foreach (var match in matches) { //extract the filename from the src var filePath = HttpUtility.UrlDecode(match.Groups[2].Value); if (string.IsNullOrEmpty(filePath)) continue; var fileName = Path.GetFileName(filePath); if(string.IsNullOrEmpty(fileName)) continue; if (!pointerMap.ContainsKey(fileName)) continue; var pointer = pointerMap[fileName]; embeddedFileNames.Add(fileName); //save to modified 'src' path var path = Path.Combine(baseUrl, pointer); if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } path = Path.Combine(path, fileName); if (!File.Exists(path)) { //get the content and write it to the path GetEmbeddedFile(pointer, path, account, configuration, senderAddress, serverName, serverPort, "", encryptKey2, userAgent); } //replace the path var newPath = "file:///" + path.Replace("\\", "/"); //make sure the src value is wrapped with quotes if (content.Contains("src=" + filePath)) { //no - wrap with single quotes newPath = "'" + newPath + "'"; } content = content.Replace(filePath, newPath); } return content; }
internal static string FetchEmbeddedImages(string content, List<Match> matches, string baseUrl, Account account, EcsConfiguration configuration, string senderAddress, string serverName, string serverPort, string encryptKey2, string userAgent) { //extract the src paths, if any foreach (var match in matches) { //extract the link from the src var ptrMatch = MatchPointer(match.Groups[2].Value); var pointer = ptrMatch.Groups[1].Value; var fileName = HttpUtility.UrlDecode(ptrMatch.Groups[2].Value); if (string.IsNullOrEmpty(fileName) || string.IsNullOrEmpty(pointer)) continue; //save to modified 'src' path var path = Path.Combine(baseUrl, pointer); if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } path = Path.Combine(path, fileName); if (!File.Exists(path)) { //get the content and write it to the path GetEmbeddedFile(pointer, path, account, configuration, senderAddress, serverName, serverPort, "", encryptKey2, userAgent); } //read the bytes, base64 encode var data = Convert.ToBase64String(File.ReadAllBytes(path)); var newValue = match.Groups[1].Value + string.Format("data:image/{0};base64,{1}", Path.GetExtension(path), data) + match.Groups[3].Value; //make sure we keep the pointer in the alt attribute //replace the link with base64 data protocol content = content.Replace(match.Value, newValue); } return content; }
public static bool PostAttachments(Outlook.MailItem item, Account account, EcsConfiguration configuration, string encryptKey, string recips, ref List<string> pointers, OutlookWin32Window win, bool noPlaceholder) { const string SOURCE = CLASS_NAME + "PostAttachments"; try { //get the bytes for the placeholder text var placeholder = Encoding.UTF8.GetBytes(Resources.placeholder_text); SafeMailItem safMail; try { safMail = RedemptionLoader.new_SafeMailItem(); } catch (Exception ex) { Logger.Error(SOURCE, String.Format( "unable to work with attachments for {0}, failed to instantiate SafeMailItem: {1}", item.Subject, ex.Message)); return false; } //need to save the item first before we can work with the SafeMailItem item.Save(); safMail.Item = item; var colAttach = safMail.Attachments; /* Outlook will move any embedded images to the head of the attachments table * if that's the case then we need to remove and re-add the other attachments * so that the pointer list will match the finished order */ var hidden = false; string contentId; var savedAttach = new Dictionary<int, byte[]>(); //do we have any embedded images? foreach (Redemption.Attachment rdoAttach in colAttach) { GetAttachProps(rdoAttach, out contentId, out hidden); if (hidden) break; } if (hidden) { //walk through in reverse order //delete and reattach each non-hidden attachment for (var i = colAttach.Count; i > 0; i--) { Redemption.Attachment rdoAttach = colAttach[i]; GetAttachProps(rdoAttach, out contentId, out hidden); if (hidden) continue; if (rdoAttach.Type.Equals(5)) //embedded { var msg = rdoAttach.EmbeddedMsg; rdoAttach.Delete(); colAttach.Add(msg, 5); } else { var path = Path.Combine(Path.GetTempPath(), "ChiaraMail", rdoAttach.FileName); var displayName = rdoAttach.DisplayName; if (File.Exists(path)) File.Delete(path); rdoAttach.SaveAsFile(path); rdoAttach.Delete(); rdoAttach = colAttach.Add(path, 1, Type.Missing, displayName); //get the bytes and drop those in the dictionary, linked to the current index savedAttach.Add(rdoAttach.Index, File.ReadAllBytes(path)); File.Delete(path); } } } //now loop through and collect the content (except for embedded messages) var attachList = new List<Attachment>(); bool showForm = false; foreach (Redemption.Attachment rdoAttach in colAttach) { var attach = new Attachment { Type = rdoAttach.Type }; switch (rdoAttach.Type) { case (int)Outlook.OlAttachmentType.olEmbeddeditem: //is this an ECS attachment? var msg = rdoAttach.EmbeddedMsg; if (HasChiaraHeader(msg)) { ForwardEmbeddedECS(msg, recips, account); } //always add attachList.Add(attach); break; case (int)Outlook.OlAttachmentType.olByReference: case (int)Outlook.OlAttachmentType.olOLE: attachList.Add(attach); break; case (int)Outlook.OlAttachmentType.olByValue: showForm = true; //we may have already gotten the bytes if (savedAttach.Count > 0 && savedAttach.ContainsKey(rdoAttach.Index)) { attach.Content = savedAttach[rdoAttach.Index]; } if (attach.Content == null || attach.Content.Length == 0) { //try just read the bytes from the binary property //this could fail if the attachment is too big try { attach.Content = rdoAttach.AsArray != null ? rdoAttach.AsArray as byte[] : null;//.Fields[ThisAddIn.PR_ATTACH_DATA_BIN]); } catch { attach.Content = null; } } if (attach.Content == null) { //save to disk then get the bytes var path = Path.Combine(Path.GetTempPath(), "ChiaraMail", rdoAttach.FileName); if (File.Exists(path)) File.Delete(path); rdoAttach.SaveAsFile(path); attach.Content = File.ReadAllBytes(path); File.Delete(path); } if (attach.Content != null) { attach.Index = rdoAttach.Index; attach.Name = rdoAttach.DisplayName; attachList.Add(attach); } else { Logger.Warning(SOURCE, "aborting: failed to retrieve content for " + rdoAttach.DisplayName); MessageBox.Show(String.Format( "Unable to retrieve original content from {0}", rdoAttach.DisplayName), Resources.product_name, MessageBoxButtons.OK, MessageBoxIcon.Error); return false; } break; } } if (!showForm) { pointers.AddRange(attachList.Select(attach => attach.Pointer)); return true; } //use the WaitForm to upload the attachments var form = new WaitForm { Attachments = attachList, Account = account, Configuration = configuration, Recips = recips, EncryptKey2 = encryptKey }; //use encryptKey2 for new post if (form.ShowDialog(win) == DialogResult.OK) { //post succeeded for all attachments //get the pointers pointers.AddRange(form.Attachments.Select(attach => attach.Pointer)); //don't replace attachment bytes if we are sending content if (noPlaceholder) return true; //loop back through to replace the original content with the placeholder bytes foreach (Redemption.Attachment rdoAttach in colAttach) { if (rdoAttach.Type.Equals(1)) //OlAttachmentType.olByValue) { rdoAttach.Fields[ThisAddIn.PR_ATTACH_DATA_BIN] = placeholder; } } return true; } //get the pointer list anyway so we can delete the items that got posted pointers.AddRange(form.Attachments .TakeWhile(attach => !String.IsNullOrEmpty(attach.Pointer)) .Select(attach => attach.Pointer)); } catch (Exception ex) { Logger.Error(SOURCE, ex.ToString()); } return false; }
/// <summary> /// to update account storage value as how much space account is left /// </summary> /// <param name="account"></param> public static void UpdateAccountStorage(Account account) { string strResponseData = ContentHandler.GetDataResponse(account.SMTPAddress, account.Configurations[0].Password, account.Configurations[0].Server, account.Configurations[0].Port); if (strResponseData.StartsWith("6 ")) { account.Storage = strResponseData.Substring(strResponseData.IndexOf("= ") + 2); } }
private static void GetEmbeddedFile(string pointer, string path, Account account, EcsConfiguration configuration, string senderAddress, string serverName, string serverPort, string encryptKey, string encryptKey2, string userAgent) { const string SOURCE = CLASS_NAME + "GetEmbeddedFile"; try { if (File.Exists(path)) return; string content; string error; ContentHandler.FetchContent(account.SMTPAddress, configuration, senderAddress, pointer, serverName, serverPort, true, out content, out error); if (string.IsNullOrEmpty(content)) { Logger.Warning(SOURCE, string.Format( "failed to retrieve image {0} using pointer {1} from {2}: {3}", Path.GetFileName(path), pointer, senderAddress, error)); return; } ContentHandler.SaveAttachment(content, encryptKey, encryptKey2, userAgent, path); } catch (Exception ex) { Logger.Error(SOURCE, ex.ToString()); } }
public static void ForwardEmbeddedECS(MessageItem msg, string recips, Account account) { string pointerString; string serverName; string serverPort; string encryptKey2; string userAgent; GetChiaraHeaders(msg, out pointerString, out serverName, out serverPort, out encryptKey2, out userAgent); var sender = msg.Sender.SMTPAddress; var config = account.Configurations.Values. First(cfg => cfg.Server.Equals(serverName, StringComparison.CurrentCultureIgnoreCase)); if (string.IsNullOrEmpty(sender)) { Logger.Warning("ForwardEmbeddedECS", string.Format( "failed to retrieve sender for {0}, skipping call to AddRecipients", msg.Subject)); return; } if (string.IsNullOrEmpty(pointerString)) { Logger.Warning("ForwardEmbeddedECS", string.Format( "failed to retrieve pointer(s) for {0}, skipping call to AddRecipients", msg.Subject)); return; } var pointers = pointerString.Split(new char[' ']); foreach (var pointer in pointers) { string error; ContentHandler.AddRecipients(account.SMTPAddress, config, sender, pointer, serverName, serverPort, recips, out error); } }
internal static string LoadEmbeddedVideos(string content, List<Match> matches, Dictionary<string, Attachment> attachList, string baseUrl, Account account, EcsConfiguration configuration, string senderAddress, string serverName, string serverPort, string encryptKey2, string userAgent) { //extract the src paths foreach (var match in matches) { //extract the filename from the src var filePath = match.Groups[2].Value; var fileName = HttpUtility.UrlDecode(filePath.Substring(filePath.LastIndexOf("/")).Replace("/", "")); if (string.IsNullOrEmpty(fileName)) continue; //find pointer for matching attachment var pointer = GetAttachPointer(attachList, fileName); if (string.IsNullOrEmpty(pointer)) continue; //save to modified 'src' path var path = Path.Combine(baseUrl, pointer); if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } path = Path.Combine(path, fileName); if (!File.Exists(path)) { //get the content and write it to the path GetEmbeddedFile(pointer, path, account, configuration, senderAddress, serverName, serverPort, "", encryptKey2, userAgent); } //read the bytes, base64 encode var data = Convert.ToBase64String(File.ReadAllBytes(path)); var ext = Path.GetExtension(path); if (!string.IsNullOrEmpty(ext)) ext = ext.Replace(".", ""); var dataUri = string.Format("data:video/{0};base64,{1}", ext, data); //update the src link with the local path content = content.Replace(filePath, dataUri); } return content; }
public static void DeletePointers(List<string> pointers, Account account, EcsConfiguration configuration) { const string SOURCE = CLASS_NAME + "DeletePointers"; Logger.Info(SOURCE, String.Format( "deleting {0} pointers after failed attachment upload", pointers.Count)); foreach (var pointer in pointers) { string error; ContentHandler.DeleteContent(account.SMTPAddress, configuration, pointer, out error, true); } }
internal static void LoadAttachments(Outlook.MailItem item, string[] pointers,string baseUrl, Account account, string senderAddress, string serverName, string serverPort, string encryptKey, string encryptKey2, List<string> EmbeddedFileNames, ref Dictionary<string,Attachment> attachList, ref Dictionary<string, Redemption.Attachment> embedded, ref Panel panelAttach, ref int upperWidth, ref int upperHeight) { const string SOURCE = CLASS_NAME + "LoadAttachments"; SafeMailItem safMail; try { safMail = RedemptionLoader.new_SafeMailItem(); } catch (Exception ex) { Logger.Warning(SOURCE,string.Format( "unable to load attachments for {0}, error instantiating SafeMailItem: {1}", item.Subject,ex.Message)); return; } safMail.Item = item; var safAttachments = safMail.Attachments; var index = 0; foreach (Redemption.Attachment safAttach in safAttachments) { index++; var attach = new Attachment { Index = index, Name = safAttach.DisplayName }; if (EmbeddedFileNames.Contains(safAttach.DisplayName)) continue; var btn = new AttachPanel { Caption = safAttach.DisplayName }; switch (safAttach.Type) { case 1: //byvalue if (string.IsNullOrEmpty(pointers[index])) continue; //is it hidden or have contentId string contentId; bool hidden; GetAttachProps(safAttach, out contentId, out hidden); if (hidden) { continue; } attach.Pointer = pointers[index]; //get the image var container = ShellIcons.GetIconForFile( attach.Name, true, false); btn.Picture = container.Icon.ToBitmap(); btn.Pointer = attach.Pointer; attachList.Add(attach.Pointer, attach); break; case 5: //embedded - will have null/0 pointer btn.Pointer = "embedded:" + index.ToString(CultureInfo.InvariantCulture); embedded.Add(btn.Pointer, safAttach); //use default envelope picture for button break; } panelAttach.Controls.Add(btn); if (btn.Width > upperWidth) upperWidth = btn.Width; if (btn.Height > upperHeight) upperHeight = btn.Height; } }