/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nscore.h" #include "nsMsgUtils.h" #include "nsArrayUtils.h" #include "nsMessengerOSXIntegration.h" #include "nsIMsgMailSession.h" #include "nsIMsgIncomingServer.h" #include "nsIMsgIdentity.h" #include "nsIMsgAccount.h" #include "nsIMsgFolder.h" #include "nsCOMPtr.h" #include "nsMsgBaseCID.h" #include "nsMsgFolderFlags.h" #include "nsDirectoryServiceDefs.h" #include "nsIDirectoryService.h" #include "MailNewsTypes.h" #include "nsIWindowMediator.h" #include "nsIDOMChromeWindow.h" #include "mozIDOMWindow.h" #include "nsPIDOMWindow.h" #include "nsIDocShell.h" #include "nsIBaseWindow.h" #include "nsIWidget.h" #include "nsIObserverService.h" #include "nsIPrefService.h" #include "nsIPrefBranch.h" #include "nsIMessengerWindowService.h" #include "prprf.h" #include "nsIAlertsService.h" #include "nsIStringBundle.h" #include "nsToolkitCompsCID.h" #include "nsIMsgDatabase.h" #include "nsIMsgHdr.h" #include "nsISupportsPrimitives.h" #include "nsIWindowWatcher.h" #include "nsMsgLocalCID.h" #include "nsIMsgMailNewsUrl.h" #include "nsIMsgWindow.h" #include "nsIMsgAccountManager.h" #include "nsIMessenger.h" #include "nsObjCExceptions.h" #include "nsComponentManagerUtils.h" #include "nsServiceManagerUtils.h" #include "mozINewMailNotificationService.h" #include "mozilla/mailnews/MimeHeaderParser.h" #include #import #define kBiffAnimateDockIconPref "mail.biff.animate_dock_icon" #define kMaxDisplayCount 10 using namespace mozilla::mailnews; // HACK: Limitations in Focus/SetFocus on Mac (see bug 465446) nsresult FocusAppNative() { ProcessSerialNumber psn; if (::GetCurrentProcess(&psn) != 0) return NS_ERROR_FAILURE; if (::SetFrontProcess(&psn) != 0) return NS_ERROR_FAILURE; return NS_OK; } static void openMailWindow(const nsCString& aUri) { nsresult rv; nsCOMPtr mailSession ( do_GetService(NS_MSGMAILSESSION_CONTRACTID, &rv)); if (NS_FAILED(rv)) return; nsCOMPtr topMostMsgWindow; rv = mailSession->GetTopmostMsgWindow(getter_AddRefs(topMostMsgWindow)); if (topMostMsgWindow) { if (!aUri.IsEmpty()) { nsCOMPtr msgUri(do_CreateInstance(NS_MAILBOXURL_CONTRACTID, &rv)); if (NS_FAILED(rv)) return; rv = msgUri->SetSpec(aUri); if (NS_FAILED(rv)) return; bool isMessageUri = false; msgUri->GetIsMessageUri(&isMessageUri); if (isMessageUri) { nsCOMPtr wwatch(do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv)); if (NS_FAILED(rv)) return; // SeaMonkey only supports message uris, whereas Thunderbird only // supports message headers. This should be simplified/removed when // bug 507593 is implemented. #ifdef MOZ_SUITE nsCOMPtr newWindow; wwatch->OpenWindow(0, "chrome://messenger/content/messageWindow.xul", "_blank", "all,chrome,dialog=no,status,toolbar", msgUri, getter_AddRefs(newWindow)); #else nsCOMPtr messenger(do_CreateInstance(NS_MESSENGER_CONTRACTID, &rv)); if (NS_FAILED(rv)) return; nsCOMPtr msgHdr; messenger->MsgHdrFromURI(aUri, getter_AddRefs(msgHdr)); if (msgHdr) { nsCOMPtr newWindow; wwatch->OpenWindow(0, "chrome://messenger/content/messageWindow.xul", "_blank", "all,chrome,dialog=no,status,toolbar", msgHdr, getter_AddRefs(newWindow)); } #endif } else { nsCOMPtr windowCommands; topMostMsgWindow->GetWindowCommands(getter_AddRefs(windowCommands)); if (windowCommands) windowCommands->SelectFolder(aUri); } } FocusAppNative(); nsCOMPtr domWindow; topMostMsgWindow->GetDomWindow(getter_AddRefs(domWindow)); if (domWindow) { nsCOMPtr privateWindow = nsPIDOMWindowOuter::From(domWindow); privateWindow->Focus(); } } else { // the user doesn't have a mail window open already so open one for them... nsCOMPtr messengerWindowService = do_GetService(NS_MESSENGERWINDOWSERVICE_CONTRACTID); // if we want to preselect the first account with new mail, // here is where we would try to generate a uri to pass in // (and add code to the messenger window service to make that work) if (messengerWindowService) messengerWindowService->OpenMessengerWindowWithUri( "mail:3pane", aUri.get(), nsMsgKey_None); } } nsMessengerOSXIntegration::nsMessengerOSXIntegration() { mBiffStateAtom = MsgGetAtom("BiffState"); mNewMailReceivedAtom = MsgGetAtom("NewMailReceived"); mUnreadTotal = 0; } nsMessengerOSXIntegration::~nsMessengerOSXIntegration() { RestoreDockIcon(); } NS_IMPL_ADDREF(nsMessengerOSXIntegration) NS_IMPL_RELEASE(nsMessengerOSXIntegration) NS_INTERFACE_MAP_BEGIN(nsMessengerOSXIntegration) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIMessengerOSIntegration) NS_INTERFACE_MAP_ENTRY(nsIMessengerOSIntegration) NS_INTERFACE_MAP_ENTRY(nsIFolderListener) NS_INTERFACE_MAP_ENTRY(nsIObserver) NS_INTERFACE_MAP_ENTRY(mozINewMailListener) NS_INTERFACE_MAP_END nsresult nsMessengerOSXIntegration::Init() { nsresult rv; nsCOMPtr observerService = do_GetService("@mozilla.org/observer-service;1", &rv); NS_ENSURE_SUCCESS(rv, rv); return observerService->AddObserver(this, "mail-startup-done", false); } NS_IMETHODIMP nsMessengerOSXIntegration::OnItemPropertyChanged(nsIMsgFolder *, nsIAtom *, char const *, char const *) { return NS_OK; } NS_IMETHODIMP nsMessengerOSXIntegration::OnItemUnicharPropertyChanged(nsIMsgFolder *, nsIAtom *, const char16_t *, const char16_t *) { return NS_OK; } NS_IMETHODIMP nsMessengerOSXIntegration::OnItemRemoved(nsIMsgFolder *, nsISupports *) { return NS_OK; } NS_IMETHODIMP nsMessengerOSXIntegration::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { if (!strcmp(aTopic, "alertfinished")) return OnAlertFinished(); if (!strcmp(aTopic, "alertclickcallback")) return OnAlertClicked(aData); #ifdef MOZ_SUITE // SeaMonkey does most of the GUI work in JS code when clicking on a mail // notification, so it needs an extra function here if (!strcmp(aTopic, "alertclicksimplecallback")) return OnAlertClickedSimple(); #endif if (!strcmp(aTopic, "mail-startup-done")) { nsresult rv; nsCOMPtr observerService = do_GetService("@mozilla.org/observer-service;1", &rv); if (NS_SUCCEEDED(rv)) { observerService->RemoveObserver(this, "mail-startup-done"); nsCOMPtr pref(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); } // Register with the new mail service for changes to the unread message count nsCOMPtr newmail = do_GetService(MOZ_NEWMAILNOTIFICATIONSERVICE_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); // This should really be an assert with a helpful message rv = newmail->AddListener(this, mozINewMailNotificationService::count); NS_ENSURE_SUCCESS(rv, rv); // This should really be an assert with a helpful message // Get the initial unread count. Ignore return value; if code above didn't fail, this won't rv = newmail->GetMessageCount(&mUnreadTotal); BadgeDockIcon(); // register with the mail sesson for folder events // we care about new count, biff status nsCOMPtr mailSession = do_GetService(NS_MSGMAILSESSION_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); return mailSession->AddFolderListener(this, nsIFolderListener::boolPropertyChanged | nsIFolderListener::intPropertyChanged); } return NS_OK; } nsresult nsMessengerOSXIntegration::GetStringBundle(nsIStringBundle **aBundle) { NS_ENSURE_ARG_POINTER(aBundle); nsresult rv; nsCOMPtr bundleService = do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv); nsCOMPtr bundle; if (bundleService && NS_SUCCEEDED(rv)) bundleService->CreateBundle("chrome://messenger/locale/messenger.properties", getter_AddRefs(bundle)); bundle.swap(*aBundle); return rv; } void nsMessengerOSXIntegration::FillToolTipInfo(nsIMsgFolder *aFolder, int32_t aNewCount) { if (aFolder) { nsString authors; int32_t numNotDisplayed; nsresult rv = GetNewMailAuthors(aFolder, authors, aNewCount, &numNotDisplayed); // If all senders are vetoed, the authors string will be empty. if (NS_FAILED(rv) || authors.IsEmpty()) return; // If this isn't the root folder, get it so we can report for it. // GetRootFolder always returns the server's root, so calling on the root itself is fine. nsCOMPtr rootFolder; aFolder->GetRootFolder(getter_AddRefs(rootFolder)); if (!rootFolder) return; nsString accountName; rootFolder->GetPrettiestName(accountName); nsCOMPtr bundle; GetStringBundle(getter_AddRefs(bundle)); if (bundle) { nsAutoString numNewMsgsText; numNewMsgsText.AppendInt(aNewCount); nsString finalText; nsCString uri; aFolder->GetURI(uri); if (numNotDisplayed > 0) { nsAutoString numNotDisplayedText; numNotDisplayedText.AppendInt(numNotDisplayed); const char16_t *formatStrings[3] = { numNewMsgsText.get(), authors.get(), numNotDisplayedText.get() }; bundle->FormatStringFromName(u"macBiffNotification_messages_extra", formatStrings, 3, getter_Copies(finalText)); } else { const char16_t *formatStrings[2] = { numNewMsgsText.get(), authors.get() }; if (aNewCount == 1) { bundle->FormatStringFromName(u"macBiffNotification_message", formatStrings, 2, getter_Copies(finalText)); // Since there is only 1 message, use the most recent mail's URI instead of the folder's nsCOMPtr db; rv = aFolder->GetMsgDatabase(getter_AddRefs(db)); if (NS_SUCCEEDED(rv) && db) { uint32_t numNewKeys; uint32_t *newMessageKeys; rv = db->GetNewList(&numNewKeys, &newMessageKeys); if (NS_SUCCEEDED(rv)) { nsCOMPtr hdr; rv = db->GetMsgHdrForKey(newMessageKeys[numNewKeys - 1], getter_AddRefs(hdr)); if (NS_SUCCEEDED(rv) && hdr) aFolder->GetUriForMsg(hdr, uri); } NS_Free(newMessageKeys); } } else bundle->FormatStringFromName(u"macBiffNotification_messages", formatStrings, 2, getter_Copies(finalText)); } ShowAlertMessage(accountName, finalText, uri); } // if we got a bundle } // if we got a folder } nsresult nsMessengerOSXIntegration::ShowAlertMessage(const nsAString& aAlertTitle, const nsAString& aAlertText, const nsACString& aFolderURI) { nsresult rv; nsCOMPtr alertsService (do_GetService(NS_ALERTSERVICE_CONTRACTID, &rv)); // If we have an nsIAlertsService implementation, use it: if (NS_SUCCEEDED(rv)) { alertsService->ShowAlertNotification(EmptyString(), aAlertTitle, aAlertText, true, NS_ConvertASCIItoUTF16(aFolderURI), this, EmptyString(), NS_LITERAL_STRING("auto"), EmptyString(), EmptyString(), nullptr, false, false); } BounceDockIcon(); if (NS_FAILED(rv)) OnAlertFinished(); return rv; } NS_IMETHODIMP nsMessengerOSXIntegration::OnItemIntPropertyChanged(nsIMsgFolder *aFolder, nsIAtom *aProperty, int64_t aOldValue, int64_t aNewValue) { // if we got new mail show an alert if (aNewValue == nsIMsgFolder::nsMsgBiffState_NewMail) { bool performingBiff = false; nsCOMPtr server; aFolder->GetServer(getter_AddRefs(server)); if (server) server->GetPerformingBiff(&performingBiff); if (!performingBiff) return NS_OK; // kick out right now... // Biff happens for the root folder, but we want info for the child with new mail nsCString folderUri; GetFirstFolderWithNewMail(aFolder, folderUri); nsCOMPtr childFolder; nsresult rv = aFolder->GetChildWithURI(folderUri, true, true, getter_AddRefs(childFolder)); if (NS_FAILED(rv) || !childFolder) return NS_ERROR_FAILURE; int32_t numNewMessages = 0; childFolder->GetNumNewMessages(true, &numNewMessages); FillToolTipInfo(childFolder, numNewMessages); } else if (mNewMailReceivedAtom == aProperty) { FillToolTipInfo(aFolder, aNewValue); } return NS_OK; } nsresult nsMessengerOSXIntegration::OnAlertClicked(const char16_t* aAlertCookie) { openMailWindow(NS_ConvertUTF16toUTF8(aAlertCookie)); return NS_OK; } #ifdef MOZ_SUITE nsresult nsMessengerOSXIntegration::OnAlertClickedSimple() { // SeaMonkey only function; only focus the app here, rest of the work will // be done in suite/mailnews/mailWidgets.xml FocusAppNative(); return NS_OK; } #endif nsresult nsMessengerOSXIntegration::OnAlertFinished() { return NS_OK; } nsresult nsMessengerOSXIntegration::BounceDockIcon() { nsresult rv; nsCOMPtr prefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); NS_ENSURE_SUCCESS(rv, rv); bool bounceDockIcon = false; rv = prefBranch->GetBoolPref(kBiffAnimateDockIconPref, &bounceDockIcon); NS_ENSURE_SUCCESS(rv, rv); if (!bounceDockIcon) return NS_OK; nsCOMPtr mediator(do_GetService(NS_WINDOWMEDIATOR_CONTRACTID)); if (mediator) { nsCOMPtr domWindow; mediator->GetMostRecentWindow(u"mail:3pane", getter_AddRefs(domWindow)); if (domWindow) { nsCOMPtr chromeWindow(do_QueryInterface(domWindow)); chromeWindow->GetAttention(); } } return NS_OK; } nsresult nsMessengerOSXIntegration::RestoreDockIcon() { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; id tile = [[NSApplication sharedApplication] dockTile]; [tile setBadgeLabel: nil]; return NS_OK; NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; } nsresult nsMessengerOSXIntegration::BadgeDockIcon() { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; int32_t unreadCount = mUnreadTotal; // If count is less than one, we should restore the original dock icon. if (unreadCount < 1) { RestoreDockIcon(); return NS_OK; } // Draw the number, first giving extensions a chance to modify. // Extensions might wish to transform "1000" into "100+" or some // other short string. Getting back the empty string will cause // nothing to be drawn and us to return early. nsresult rv; nsCOMPtr os (do_GetService("@mozilla.org/observer-service;1", &rv)); if (NS_FAILED(rv)) { RestoreDockIcon(); return rv; } nsCOMPtr str (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID, &rv)); if (NS_FAILED(rv)) { RestoreDockIcon(); return rv; } nsAutoString total; total.AppendInt(unreadCount); str->SetData(total); os->NotifyObservers(str, "before-unread-count-display", total.get()); nsAutoString badgeString; str->GetData(badgeString); if (badgeString.IsEmpty()) { RestoreDockIcon(); return NS_OK; } id tile = [[NSApplication sharedApplication] dockTile]; [tile setBadgeLabel:[NSString stringWithFormat:@"%S", (const unichar*)badgeString.get()]]; return NS_OK; NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; } NS_IMETHODIMP nsMessengerOSXIntegration::OnItemPropertyFlagChanged(nsIMsgDBHdr *item, nsIAtom *property, uint32_t oldFlag, uint32_t newFlag) { return NS_OK; } NS_IMETHODIMP nsMessengerOSXIntegration::OnItemAdded(nsIMsgFolder *, nsISupports *) { return NS_OK; } NS_IMETHODIMP nsMessengerOSXIntegration::OnItemBoolPropertyChanged(nsIMsgFolder *aItem, nsIAtom *aProperty, bool aOldValue, bool aNewValue) { return NS_OK; } NS_IMETHODIMP nsMessengerOSXIntegration::OnItemEvent(nsIMsgFolder *, nsIAtom *) { return NS_OK; } nsresult nsMessengerOSXIntegration::GetNewMailAuthors(nsIMsgFolder* aFolder, nsString& aAuthors, int32_t aNewCount, int32_t* aNotDisplayed) { // Get a list of names or email addresses for the folder's authors // with new mail. Note that we only process the most recent "new" // mail (aNewCount), working from most recently added. Duplicates // are removed, and names are displayed to a set limit // (kMaxDisplayCount) with the remaining count being returned in // aNotDisplayed. Extension developers can listen for // "newmail-notification-requested" and then make a decision about // including a given author or not. As a result, it is possible that // the resulting length of aAuthors will be 0. nsCOMPtr db; nsresult rv = aFolder->GetMsgDatabase(getter_AddRefs(db)); uint32_t numNewKeys = 0; if (NS_SUCCEEDED(rv) && db) { nsCOMPtr os = do_GetService("@mozilla.org/observer-service;1", &rv); NS_ENSURE_SUCCESS(rv, rv); // Get proper l10n list separator -- ", " in English nsCOMPtr bundle; GetStringBundle(getter_AddRefs(bundle)); if (!bundle) return NS_ERROR_FAILURE; uint32_t *newMessageKeys; rv = db->GetNewList(&numNewKeys, &newMessageKeys); if (NS_SUCCEEDED(rv)) { nsString listSeparator; bundle->GetStringFromName(u"macBiffNotification_separator", getter_Copies(listSeparator)); int32_t displayed = 0; for (int32_t i = numNewKeys - 1; i >= 0; i--, aNewCount--) { if (0 == aNewCount || displayed == kMaxDisplayCount) break; nsCOMPtr hdr; rv = db->GetMsgHdrForKey(newMessageKeys[i], getter_AddRefs(hdr)); if (NS_SUCCEEDED(rv) && hdr) { nsString author; rv = hdr->GetMime2DecodedAuthor(author); if (NS_FAILED(rv)) continue; nsString name; ExtractName(DecodedHeader(author), name); // Give extensions a chance to suppress notifications for this author nsCOMPtr notify = do_CreateInstance(NS_SUPPORTS_PRBOOL_CONTRACTID); notify->SetData(true); os->NotifyObservers(notify, "newmail-notification-requested", author.get()); bool includeSender; notify->GetData(&includeSender); // Don't add unwanted or duplicate names if (includeSender && aAuthors.Find(name, true) == -1) { if (displayed > 0) aAuthors.Append(listSeparator); aAuthors.Append(name); displayed++; } } } } NS_Free(newMessageKeys); } *aNotDisplayed = aNewCount; return rv; } nsresult nsMessengerOSXIntegration::GetFirstFolderWithNewMail(nsIMsgFolder* aFolder, nsCString& aFolderURI) { // Find the subfolder in aFolder with new mail and return the folderURI if (aFolder) { nsCOMPtr msgFolder; // enumerate over the folders under this root folder till we find one with new mail.... nsCOMPtr allFolders; nsresult rv = aFolder->GetDescendants(getter_AddRefs(allFolders)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr enumerator; rv = allFolders->Enumerate(getter_AddRefs(enumerator)); if (NS_SUCCEEDED(rv) && enumerator) { nsCOMPtr supports; int32_t numNewMessages = 0; bool hasMore = false; while (NS_SUCCEEDED(enumerator->HasMoreElements(&hasMore)) && hasMore) { rv = enumerator->GetNext(getter_AddRefs(supports)); if (NS_SUCCEEDED(rv) && supports) { msgFolder = do_QueryInterface(supports, &rv); if (msgFolder) { numNewMessages = 0; msgFolder->GetNumNewMessages(false, &numNewMessages); if (numNewMessages) break; // kick out of the while loop } } // if we have a folder } // if we have more potential folders to enumerate } // if enumerator if (msgFolder) msgFolder->GetURI(aFolderURI); } return NS_OK; } /* * Method implementations for mozINewMailListener */ NS_IMETHODIMP nsMessengerOSXIntegration::OnCountChanged(uint32_t count) { mUnreadTotal = count; BadgeDockIcon(); return NS_OK; }