[Pkg-mozext-commits] [firetray] 26/84: * creation of basic icon with text * load .bmp in addition to .ico (and load them LR_SHARED) * use ctypes pointer.isNull()

David Prévot taffit at moszumanska.debian.org
Sun Jul 20 01:42:43 UTC 2014


This is an automated email from the git hooks/post-receive script.

taffit pushed a commit to branch master
in repository firetray.

commit 70f0c0a8e06c04c322a0b44db30504baffa3e8a2
Author: foudfou <foudil.newbie+git at gmail.com>
Date:   Fri Feb 14 00:22:46 2014 +0100

    * creation of basic icon with text
    * load .bmp in addition to .ico (and load them LR_SHARED)
    * use ctypes pointer.isNull()
---
 src/chrome/skin/winnt/blank-icon.bmp     | Bin 0 -> 3126 bytes
 src/modules/FiretrayMessaging.jsm        |   2 +
 src/modules/ctypes/winnt/gdi32.jsm       |  93 ++++++++++++++++++++
 src/modules/ctypes/winnt/user32.jsm      |   7 ++
 src/modules/ctypes/winnt/win32.jsm       |  17 +++-
 src/modules/winnt/FiretrayStatusIcon.jsm | 142 ++++++++++++++++++++++---------
 6 files changed, 217 insertions(+), 44 deletions(-)

diff --git a/src/chrome/skin/winnt/blank-icon.bmp b/src/chrome/skin/winnt/blank-icon.bmp
new file mode 100644
index 0000000..ce34799
Binary files /dev/null and b/src/chrome/skin/winnt/blank-icon.bmp differ
diff --git a/src/modules/FiretrayMessaging.jsm b/src/modules/FiretrayMessaging.jsm
index a8adfaf..6b06e89 100644
--- a/src/modules/FiretrayMessaging.jsm
+++ b/src/modules/FiretrayMessaging.jsm
@@ -206,6 +206,7 @@ firetray.Messaging = {
   },
 
   updateIcon: function(msgCount) {
+    log.debug("updateIcon");
     if ("undefined" === typeof(msgCount)) msgCount = this.currentMsgCount;
 
     let localizedTooltip;
@@ -227,6 +228,7 @@ firetray.Messaging = {
 
     } else if (msgCount > 0) {
       let prefMailNotification = firetray.Utils.prefService.getIntPref('mail_notification_type');
+      log.debug("msgCount prefMailNotification="+prefMailNotification);
       switch (prefMailNotification) {
       case FIRETRAY_NOTIFICATION_MESSAGE_COUNT:
         let prefIconTextColor = firetray.Utils.prefService.getCharPref("icon_text_color");
diff --git a/src/modules/ctypes/winnt/gdi32.jsm b/src/modules/ctypes/winnt/gdi32.jsm
new file mode 100644
index 0000000..82b8fd0
--- /dev/null
+++ b/src/modules/ctypes/winnt/gdi32.jsm
@@ -0,0 +1,93 @@
+var EXPORTED_SYMBOLS = [ "gdi32" ];
+
+const GDI32_LIBNAME = "gdi32";
+const GDI32_ABIS    = [ "dll" ];
+
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/ctypes.jsm");
+Cu.import("resource://firetray/ctypes/ctypes-utils.jsm");
+Cu.import("resource://firetray/ctypes/winnt/win32.jsm");
+
+function gdi32_defines(lib) {
+
+  this.BITMAP = ctypes.StructType("BITMAP", [
+    { "bmType": win32.LONG },
+    { "bmWidth": win32.LONG },
+    { "bmHeight": win32.LONG },
+    { "bmWidthBytes": win32.LONG },
+    { "bmPlanes": win32.WORD },
+    { "bmBitsPixel": win32.WORD },
+    { "bmBits": win32.LPVOID }
+  ]);
+  this.PBITMAP = this.BITMAP.ptr;
+
+  lib.lazy_bind("CreateCompatibleDC", win32.HDC, win32.HDC);
+  lib.lazy_bind("DeleteDC", win32.BOOL, win32.HDC);
+  lib.lazy_bind("CreateCompatibleBitmap", win32.HBITMAP, win32.HDC, ctypes.int, ctypes.int);
+  lib.lazy_bind("SelectObject", win32.HGDIOBJ, win32.HDC, win32.HGDIOBJ);
+  lib.lazy_bind("DeleteObject", win32.BOOL, win32.HGDIOBJ);
+  lib.lazy_bind("PatBlt", win32.BOOL, win32.HDC, ctypes.int, ctypes.int, ctypes.int, ctypes.int, win32.DWORD);
+  this.BLACKNESS = win32.DWORD(0x00000042); /* dest = BLACK */
+  this.WHITENESS = win32.DWORD(0x00FF0062); /* dest = WHITE */
+  lib.lazy_bind("CreateFontW", win32.HFONT, ctypes.int, ctypes.int, ctypes.int, ctypes.int, ctypes.int, win32.DWORD, win32.DWORD, win32.DWORD, win32.DWORD, win32.DWORD, win32.DWORD, win32.DWORD, win32.DWORD, win32.LPCWSTR);
+  this.FW_DONTCARE   = 0;
+  this.FW_THIN       = 100;
+  this.FW_EXTRALIGHT = 200;
+  this.FW_LIGHT      = 300;
+  this.FW_NORMAL     = 400;
+  this.FW_MEDIUM     = 500;
+  this.FW_SEMIBOLD   = 600;
+  this.FW_BOLD       = 700;
+  this.FW_EXTRABOLD  = 800;
+  this.FW_HEAVY      = 900;
+  this.FF_DONTCARE   = (0<<4);  /* Don't care or don't know. */
+  this.FF_ROMAN      = (1<<4);  /* Variable stroke width, serifed. Times Roman, Century Schoolbook, etc. */
+  this.FF_SWISS      = (2<<4);  /* Variable stroke width, sans-serifed. Helvetica, Swiss, etc. */
+  this.FF_MODERN     = (3<<4);  /* Constant stroke width, serifed or sans-serifed. Pica, Elite, Courier, etc. */
+  this.FF_SCRIPT     = (4<<4);  /* Cursive, etc. */
+  this.FF_DECORATIVE = (5<<4);  /* Old English, etc. */
+  this.DEFAULT_PITCH  = 0;
+  this.FIXED_PITCH    = 1;
+  this.VARIABLE_PITCH = 2;
+  this.MONO_FONT      = 8;
+  this.ANSI_CHARSET        = 0;
+  this.DEFAULT_CHARSET     = 1;
+  this.SYMBOL_CHARSET      = 2;
+  this.SHIFTJIS_CHARSET    = 128;
+  this.HANGEUL_CHARSET     = 129;
+  this.HANGUL_CHARSET      = 129;
+  this.GB2312_CHARSET      = 134;
+  this.CHINESEBIG5_CHARSET = 136;
+  this.OEM_CHARSET         = 255;
+  this.JOHAB_CHARSET       = 130;
+  this.HEBREW_CHARSET      = 177;
+  this.ARABIC_CHARSET      = 178;
+  this.GREEK_CHARSET       = 161;
+  this.TURKISH_CHARSET     = 162;
+  this.VIETNAMESE_CHARSET  = 163;
+  this.THAI_CHARSET        = 222;
+  this.EASTEUROPE_CHARSET  = 238;
+  this.RUSSIAN_CHARSET     = 204;
+  lib.lazy_bind("SetTextColor", win32.COLORREF, win32.HDC, win32.COLORREF);
+  lib.lazy_bind("SetBkMode", ctypes.int, win32.HDC, ctypes.int);
+  this.TRANSPARENT = 1;
+  this.OPAQUE      = 2;
+  this.BKMODE_LAST = 2;
+
+  lib.lazy_bind("TextOutW", win32.BOOL, win32.HDC, ctypes.int, ctypes.int, win32.LPCTSTR, ctypes.int);
+
+  lib.lazy_bind("GetTextAlign", win32.UINT, win32.HDC);
+  lib.lazy_bind("SetTextAlign", win32.UINT, win32.HDC, win32.UINT);
+  this.TA_LEFT       = 0;
+  this.TA_RIGHT      = 2;
+  this.TA_CENTER     = 6;
+  this.TA_TOP        = 0;
+  this.TA_BOTTOM     = 8;
+  this.TA_BASELINE   = 24;
+  this.TA_RTLREADING = 256;
+  this.TA_MASK       =(this.TA_BASELINE+this.TA_CENTER+this.TA_UPDATECP+this.TA_RTLREADING);
+
+}
+
+new ctypes_library(GDI32_LIBNAME, GDI32_ABIS, gdi32_defines, this);
diff --git a/src/modules/ctypes/winnt/user32.jsm b/src/modules/ctypes/winnt/user32.jsm
index 8b2abbf..6a0702e 100644
--- a/src/modules/ctypes/winnt/user32.jsm
+++ b/src/modules/ctypes/winnt/user32.jsm
@@ -51,6 +51,9 @@ function user32_defines(lib) {
   this.IDI_EXCLAMATION = win32.MAKEINTRESOURCE(32515);
   this.IDI_ASTERISK    = win32.MAKEINTRESOURCE(32516);
   lib.lazy_bind("LoadImageW", win32.HANDLE, win32.HINSTANCE, win32.LPCTSTR, win32.UINT, ctypes.int, ctypes.int, win32.UINT);
+  this.IMAGE_BITMAP        = 0;
+  this.IMAGE_ICON          = 1;
+  this.IMAGE_CURSOR        = 2;
   this.LR_CREATEDIBSECTION = 0x00002000;
   this.LR_DEFAULTCOLOR     = 0x00000000;
   this.LR_DEFAULTSIZE      = 0x00000040;
@@ -241,6 +244,10 @@ function user32_defines(lib) {
   this.SPI_GETFOREGROUNDFLASHCOUNT = 0x2004;
   lib.lazy_bind("GetForegroundWindow", win32.HWND);
 
+  lib.lazy_bind("GetDC", win32.HDC, win32.HWND);
+  lib.lazy_bind("ReleaseDC", ctypes.int, win32.HWND, win32.HDC);
+  lib.lazy_bind("CreateIconIndirect", win32.HICON, win32.PICONINFO);
+
 }
 
 new ctypes_library(USER32_LIBNAME, USER32_ABIS, user32_defines, this);
diff --git a/src/modules/ctypes/winnt/win32.jsm b/src/modules/ctypes/winnt/win32.jsm
index 05f6234..ecb7266 100644
--- a/src/modules/ctypes/winnt/win32.jsm
+++ b/src/modules/ctypes/winnt/win32.jsm
@@ -41,6 +41,10 @@ var win32 = new function() {
   this.HBRUSH    = this.HICON;
   this.HCURSOR   = this.HANDLE;
   this.HHOOK     = this.HANDLE;
+  this.HDC       = this.HANDLE;
+  this.HGDIOBJ   = this.HANDLE;
+  this.HBITMAP   = this.HANDLE;
+  this.HFONT     = this.HANDLE;
   this.TCHAR     = ctypes.jschar, // Mozilla compiled with UNICODE/_UNICODE macros and wchar_t = jschar
   this.LPSTR     = ctypes.char.ptr;
   this.LPCSTR    = ctypes.char.ptr;
@@ -52,6 +56,7 @@ var win32 = new function() {
   this.WPARAM    = this.UINT_PTR;
   this.LPARAM    = this.LONG_PTR;
   this.FARPROC   = ctypes.voidptr_t; // typedef INT_PTR (FAR WINAPI *FARPROC)();
+  this.COLORREF  = this.DWORD;       // 0x00bbggrr
 
   this.GUID = ctypes.StructType("GUID", [
     { "Data1": ctypes.unsigned_long },
@@ -97,10 +102,18 @@ var win32 = new function() {
   this.WM_MOUSELAST     = 0x020D;
   this.WM_MOUSELAST     = 0x020A;
 
+  this.ICONINFO = ctypes.StructType("ICONINFO", [
+    { "fIcon": this.BOOL },
+    { "xHotspot": this.DWORD },
+    { "yHotspot": this.DWORD },
+    { "hbmMask": this.HBITMAP },
+    { "hbmColor": this.HBITMAP }
+  ]);
+  this.PICONINFO = this.ICONINFO.ptr;
 };
 
 // ShellAPI.h
 let nin_select = win32.WM_USER + 0;
-win32.NIN_SELECT = nin_select;
-win32.NINF_KEY = 0x1;
+win32.NIN_SELECT    = nin_select;
+win32.NINF_KEY      = 0x1;
 win32.NIN_KEYSELECT = (win32.NIN_SELECT | win32.NINF_KEY);
diff --git a/src/modules/winnt/FiretrayStatusIcon.jsm b/src/modules/winnt/FiretrayStatusIcon.jsm
index 1a8f5fa..8ae1d6a 100644
--- a/src/modules/winnt/FiretrayStatusIcon.jsm
+++ b/src/modules/winnt/FiretrayStatusIcon.jsm
@@ -14,6 +14,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/ctypes.jsm");
 Cu.import("resource://firetray/ctypes/ctypesMap.jsm");
 Cu.import("resource://firetray/ctypes/winnt/win32.jsm");
+Cu.import("resource://firetray/ctypes/winnt/gdi32.jsm");
 Cu.import("resource://firetray/ctypes/winnt/kernel32.jsm");
 Cu.import("resource://firetray/ctypes/winnt/shell32.jsm");
 Cu.import("resource://firetray/ctypes/winnt/user32.jsm");
@@ -27,6 +28,7 @@ if ("undefined" == typeof(firetray.Handler))
   log.error("This module MUST be imported from/after FiretrayHandler !");
 
 FIRETRAY_ICON_CHROME_PATHS = {
+  'blank-icon': "chrome://firetray/skin/winnt/blank-icon.bmp",
   'mail-unread': "chrome://firetray/skin/winnt/mail-unread.ico",
 };
 
@@ -36,11 +38,12 @@ firetray.StatusIcon = {
   notifyIconData: null,
   hwndProxy: null,
   icons: null,
+  bitmaps: null,
   WNDCLASS_NAME: "FireTrayHiddenWindowClass",
   WNDCLASS_ATOM: null,
 
   init: function() {
-    this.loadIcons();
+    this.loadImages();
     // this.defineIconNames();     // FIXME: linux-only
     this.create();
 
@@ -52,7 +55,7 @@ firetray.StatusIcon = {
     log.debug("Disabling StatusIcon");
 
     this.destroy();
-    this.destroyIcons();
+    this.destroyImages();
 
     this.initialized = false;
     return true;
@@ -74,8 +77,9 @@ firetray.StatusIcon = {
     this.defaultNewMailIconName = "mail-unread";
   },
 
-  loadIcons: function() {
+  loadImages: function() {
     this.icons = new ctypesMap(win32.HICON);
+    this.bitmaps = new ctypesMap(win32.HBITMAP);
 
     // the Mozilla hidden window has the default Mozilla icon
     let hwnd_hidden_moz = user32.FindWindowW("MozillaHiddenWindowClass", null);
@@ -84,39 +88,39 @@ firetray.StatusIcon = {
 
     /* we'll take the first icon in the .ico file. To get the icon count in the
      file, pass ctypes.cast(ctypes.int(-1), win32.UINT); */
-    for (let ico_name in FIRETRAY_ICON_CHROME_PATHS) {
-      let path = firetray.Utils.chromeToPath(FIRETRAY_ICON_CHROME_PATHS[ico_name]);
-      let hicon = shell32.ExtractIconW(null, path, 0);
-      // ERROR_INVALID_HANDLE(6) ignored (_Reserved_ HINSTANCE hInst ?)
-      this.icons.insert(ico_name, hicon);
-      log.debug("icon '"+ico_name+"'="+this.icons.get(ico_name)+" winLastError="+ctypes.winLastError);
+    for (let imgName in FIRETRAY_ICON_CHROME_PATHS) {
+      let path = firetray.Utils.chromeToPath(FIRETRAY_ICON_CHROME_PATHS[imgName]);
+      let imgType = path.substr(-3, 3);
+      let imgTypeDict = {
+        ico: { win_t: win32.HICON,   load_const: user32.IMAGE_ICON,   map: this.icons },
+        bmp: { win_t: win32.HBITMAP, load_const: user32.IMAGE_BITMAP, map: this.bitmaps }
+      };
+      if (!(imgType in imgTypeDict)) {
+        throw Error("Unrecognized type '"+imgType+"'");
+      }
+      let imgTypeRec = imgTypeDict[imgType];
+      let himg = ctypes.cast(
+        user32.LoadImageW(null, path, imgTypeRec['load_const'], 0, 0,
+                          user32.LR_LOADFROMFILE|user32.LR_SHARED),
+        imgTypeRec['win_t']);
+      if (himg.isNull()) {
+        log.error("Could not load '"+imgName+"'="+himg+" winLastError="+ctypes.winLastError);
+        continue;
+      }
+      imgTypeRec['map'].insert(imgName, himg);
     }
   },
 
-  destroyIcons: function() {
-    let success = true, errors = [];
-    let keys = this.icons.keys;
-
-    // 'app' must not get DestroyIcon'd
-    var idx_app = keys.indexOf('app');
-    if (idx_app > -1) keys.splice(idx_app, 1);
-
-    for (let i=0, len=keys.length; i<len; ++i) {
-      let ico_name = keys[i];
-      let res = user32.DestroyIcon(this.icons.get(ico_name));
-      if (res)
-        this.icons.remove(ico_name);
-      else
-        errors.push(ctypes.winLastError);
-      success = success && res;
-    }
-    this.icons.remove('app');
-
-    if (!success) {
-      log.error("Couldn't destroy all icons: "+errors);
-    } else {
-      log.debug("Icons destroyed");
-    }
+  // images loaded with LR_SHARED need't be destroyed
+  destroyImages: function() {
+    [this.icons, this.bitmaps].forEach(function(map, idx, ary) {
+      let keys = map.keys;
+      for (let i=0, len=keys.length; i<len; ++i) {
+        let imgName = keys[i];
+        map.remove(imgName);
+      }
+    });
+    log.debug("Icons destroyed");
   },
 
   create: function() {
@@ -190,7 +194,9 @@ firetray.StatusIcon = {
         break;
       case win32.WM_RBUTTONUP:
         log.debug("WM_RBUTTONUP");
-        firetray.Handler.windowGetAttention(); // TESTING
+        let hicon = firetray.StatusIcon.createSmallIcon(hWnd, "100", "#990000");
+        log.debug("%%%%%%%%%% ICON="+hicon);
+        firetray.StatusIcon.setImageFromIcon(hicon);
         break;
       case win32.WM_CONTEXTMENU:
         log.debug("WM_CONTEXTMENU");
@@ -212,17 +218,16 @@ firetray.StatusIcon = {
     // result is a ctypes.Int64. So we need to create a CData from it before
     // casting it to a HICON.
     let icon = ctypes.cast(win32.LRESULT(rv), win32.HICON);
-    let NULL = win32.HICON(null); // for comparison only
-    if (firetray.js.strEquals(icon, NULL)) { // from the window class
+    if (icon.isNull()) { // from the window class
       rv = user32.GetClassLong(hwnd, user32.GCLP_HICONSM);
       icon = ctypes.cast(win32.ULONG_PTR(rv), win32.HICON);
       log.debug("GetClassLong winLastError="+ctypes.winLastError);
     }
-    if (firetray.js.strEquals(icon, NULL)) { // from the first resource -> ERROR_RESOURCE_TYPE_NOT_FOUND(1813)
+    if (icon.isNull()) { // from the first resource -> ERROR_RESOURCE_TYPE_NOT_FOUND(1813)
       icon = user32.LoadIconW(firetray.Win32.hInstance, win32.MAKEINTRESOURCE(0));
       log.debug("LoadIconW module winLastError="+ctypes.winLastError);
     }
-    if (firetray.js.strEquals(icon, NULL)) { // OS default icon
+    if (icon.isNull()) { // OS default icon
       icon = user32.LoadIconW(null, win32.MAKEINTRESOURCE(user32.IDI_APPLICATION));
       log.debug("LoadIconW default winLastError="+ctypes.winLastError);
     }
@@ -247,22 +252,75 @@ firetray.StatusIcon = {
     this.destroyProxyWindow();
   },
 
-  setImageFromIcon: function(icoName) {
+  setImageFromIcon: function(hicon) {
     let nid = firetray.StatusIcon.notifyIconData;
-    nid.hIcon = firetray.StatusIcon.icons.get(icoName);
+    nid.hIcon = hicon;
     rv = shell32.Shell_NotifyIconW(shell32.NIM_MODIFY, nid.address());
     log.debug("Shell_NotifyIcon MODIFY="+rv+" winLastError="+ctypes.winLastError);
+  },
+
+  // rgb colors encoded in *bbggrr*
+  cssColorToCOLORREF: function(csscolor) {
+    let rgb = csscolor.substr(1);
+    let rr = rgb.substr(0, 2);
+    let gg = rgb.substr(2, 2);
+    let bb = rgb.substr(4, 2);
+    return parseInt("0x"+bb+gg+rr);
+  },
+
+  // http://stackoverflow.com/questions/457050/how-to-display-text-in-system-tray-icon-with-win32-api
+  createSmallIcon: function(hWnd, text, color) {
+    let hdc = user32.GetDC(hWnd); // get device context (DC) for hWnd
+    let hdcMem = gdi32.CreateCompatibleDC(hdc); // creates a memory device context (DC) compatible with hdc (need a bitmap)
+    let hBitmap = this.bitmaps.get('blank-icon');
+    let hBitmapMask = gdi32.CreateCompatibleBitmap(hdc, 32, 32);
+    user32.ReleaseDC(hWnd, hdc);
+
+    let hOldBitmap = ctypes.cast(gdi32.SelectObject(hdcMem, hBitmap), // replace bitmap in hdcMem by hBitmap
+                                 win32.HBITMAP);
+    // gdi32.PatBlt(hdcMem, 0, 0, 16, 16, gdi32.BLACKNESS); // paint black rectangle
+
+    let nHeight = 32, fnWeight = gdi32.FW_BOLD;
+    let hFont = gdi32.CreateFontW(nHeight, 0, 0, 0, fnWeight, 0, 0, 0,
+      gdi32.ANSI_CHARSET, 0, 0, 0, gdi32.FF_SWISS, "Sans"); // get font
+    hFont = ctypes.cast(gdi32.SelectObject(hdcMem, hFont), win32.HFONT); // replace font in bitmap by hFont
+    gdi32.SetTextColor(hdcMem, win32.COLORREF(this.cssColorToCOLORREF(color)));
+    // gdi32.SetBkMode(hdcMem, gdi32.TRANSPARENT); // VERY IMPORTANT
+    // gdi32.SetTextAlign(hdcMem, gdi32.GetTextAlign(hdcMem) & (~gdi32.TA_CENTER));
+    // gdi32.SetTextAlign(hdcMem, gdi32.TA_CENTER);
+    log.debug("   ___ALIGN=(winLastError="+ctypes.winLastError+") "+gdi32.GetTextAlign(hdcMem));
+    gdi32.TextOutW(hdcMem, 0, 0, text, text.length);
+
+    gdi32.SelectObject(hdcMem, hOldBitmap); // always replace new hBitmap with original one
+
+    let iconInfo = win32.ICONINFO();
+    iconInfo.fIcon = true;
+    iconInfo.xHotspot = 0;
+    iconInfo.yHotspot = 0;
+    iconInfo.hbmMask = hBitmapMask;
+    iconInfo.hbmColor = hBitmap;
+
+    let hIcon = user32.CreateIconIndirect(iconInfo.address());
+
+    gdi32.DeleteObject(gdi32.SelectObject(hdcMem, hFont));
+    gdi32.DeleteDC(hdcMem);
+    // gdi32.DeleteDC(hdc); // already ReleaseDC's
+    gdi32.DeleteObject(hBitmap);
+    gdi32.DeleteObject(hBitmapMask);
+
+    return hIcon;               // to be destroyed (DestroyIcon)
   }
 
 }; // firetray.StatusIcon
 
 firetray.Handler.setIconImageDefault = function() {
   log.debug("setIconImageDefault");
-  firetray.StatusIcon.setImageFromIcon('app');
+  firetray.StatusIcon.setImageFromIcon(firetray.StatusIcon.icons.get('app'));
 };
 
 firetray.Handler.setIconImageNewMail = function() {
-  firetray.StatusIcon.setImageFromIcon('mail-unread');
+  log.debug("setIconImageDefault");
+  firetray.StatusIcon.setImageFromIcon(firetray.StatusIcon.icons.get('mail-unread'));
 };
 
 // firetray.Handler.setIconImageFromFile = firetray.StatusIcon.setIconImageFromFile;

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-mozext/firetray.git



More information about the Pkg-mozext-commits mailing list