diff --git a/Dockerfile b/Dockerfile index 0d7ba5a..470b4a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM node:20-alpine WORKDIR /app COPY package*.json ./ -RUN npm ci --omit=dev +RUN npm install --omit=dev COPY . . diff --git a/README.md b/README.md index 566bef6..8333bd0 100644 --- a/README.md +++ b/README.md @@ -17,14 +17,27 @@ Runs as a single container — designed for Unraid but works on any Docker host. --- +## Logo & Image Hosting + +All images referenced in the signature template must be publicly accessible via HTTPS. +The default logo URL is: +``` +https://alwisp.com/uploads/logo.png +``` +Upload your logo and any other signature images to `https://alwisp.com/uploads/` and +reference them by full URL in the template. The logo URL can also be changed live +in the Template Editor UI without rebuilding the container. + +--- + ## Environment Variables All secrets and configuration are passed as **environment variables at runtime**. -No `.env` file is committed to this repo — see `.env.example` for all variable names and descriptions. +No `.env` file is committed to this repo — see `.env.example` for all variable names. | Variable | Description | Default | |---|---|---| -| `GOOGLE_ADMIN_EMAIL` | Workspace admin email for Directory API | *(required)* | +| `GOOGLE_ADMIN_EMAIL` | Workspace admin email (e.g. jason@messagepoint.tv) | *(required)* | | `GOOGLE_CUSTOMER_ID` | Use `my_customer` for primary domain | `my_customer` | | `SERVICE_ACCOUNT_KEY_PATH` | Path to sa.json inside container | `/app/secrets/sa.json` | | `ADMIN_USERNAME` | Web UI login username | *(required)* | @@ -34,12 +47,12 @@ No `.env` file is committed to this repo — see `.env.example` for all variable | `NODE_ENV` | Node environment | `production` | ### On Unraid -Set each variable directly in the **Docker container template UI** under the -"Variables" section. No file needed on the host. +Set each variable directly in the **Docker container template UI** under Variables. ### For Local Development -Copy `.env.example` to `.env`, fill in your values, then run: ```bash +cp .env.example .env +# fill in values docker-compose --env-file .env up -d --build ``` @@ -47,192 +60,126 @@ docker-compose --env-file .env up -d --build ## Part 1 — Google Cloud Console Setup -This section walks through creating the service account and downloading the key -that allows the app to impersonate users in your Google Workspace domain. - ### Step 1 — Create a Google Cloud Project -1. Open https://console.cloud.google.com and sign in with your Google Workspace admin account -2. Click the **project dropdown** in the top navigation bar (next to the Google Cloud logo) -3. Click **New Project** in the top right of the dialog -4. Enter a project name: `email-sig-manager` -5. Leave the organization as your Workspace domain → click **Create** -6. Wait a few seconds, then select the new project from the dropdown to make it active - -> **Note:** If your org already has a GCP project you prefer to use, you can skip -> project creation and just enable the APIs inside the existing project. - ---- +1. Open https://console.cloud.google.com — sign in with your Google Workspace admin account +2. Click the **project dropdown** (top nav bar) → **New Project** +3. Name: `email-sig-manager` → **Create** +4. Select the new project from the dropdown to make it active ### Step 2 — Enable Required APIs -The app needs two Google APIs enabled in your project. +**Admin SDK API:** +1. Go to **APIs & Services → Library** +2. Search `Admin SDK API` → click result → **Enable** -**Enable Admin SDK API:** -1. In the GCP Console, go to **APIs & Services → Library** (left sidebar) -2. Search for `Admin SDK API` -3. Click the result → click **Enable** - -**Enable Gmail API:** +**Gmail API:** 1. Go back to **APIs & Services → Library** -2. Search for `Gmail API` -3. Click the result → click **Enable** - -Both should now show as **Enabled** under **APIs & Services → Enabled APIs & Services**. - ---- +2. Search `Gmail API` → click result → **Enable** ### Step 3 — Create the Service Account -1. In the left sidebar, go to **IAM & Admin → Service Accounts** -2. Click **+ Create Service Account** at the top -3. Fill in the details: - - **Service account name:** `email-sig-manager` - - **Service account ID:** will auto-fill as `email-sig-manager` - - **Description:** `Email signature push service for Google Workspace` -4. Click **Create and Continue** -5. On the "Grant this service account access to project" step — **skip this, click Continue** -6. On the "Grant users access" step — **skip this too, click Done** +1. Go to **IAM & Admin → Service Accounts** +2. Click **+ Create Service Account** +3. Name: `email-sig-manager` → **Create and Continue** +4. Skip role assignment → **Continue** → skip user access → **Done** -You will land back on the service accounts list and see your new account listed. +### Step 4 — Enable Domain-Wide Delegation ---- - -### Step 4 — Enable Domain-Wide Delegation on the Service Account - -1. Click the service account name (`email-sig-manager`) in the list to open it -2. Click the **Edit** button (pencil icon) at the top -3. Scroll down to find **"Show advanced settings"** — click it to expand -4. Check the box for **"Enable Google Workspace Domain-wide Delegation"** -5. Enter a product name for the consent screen if prompted (e.g., `Email Signature Manager`) +1. Click the service account name to open it +2. Click **Edit** (pencil icon) +3. Expand **Show advanced settings** +4. Check **Enable Google Workspace Domain-wide Delegation** +5. Enter product name: `Email Signature Manager` 6. Click **Save** +7. Back on the service account detail page, note the **Client ID** (long number) — copy it -After saving, return to the service account detail page. You should now see a -**"Domain-wide delegation"** line showing a **Client ID** (a long numeric string, -e.g., `112233445566778899`). **Copy this Client ID** — you will need it in Part 2. +### Step 5 — Download the JSON Key ---- - -### Step 5 — Download the Service Account JSON Key - -1. On the service account detail page, click the **Keys** tab -2. Click **Add Key → Create New Key** -3. Select **JSON** as the key type → click **Create** -4. The key file downloads automatically to your computer -5. **Rename the file to `sa.json`** -6. Store it securely — this file grants impersonation access to all users in your domain - -> This file goes in the `secrets/` folder on your Unraid host. It is gitignored -> and must never be committed to GitHub. +1. Click the **Keys** tab on the service account +2. **Add Key → Create New Key → JSON → Create** +3. Rename the downloaded file to `sa.json` +4. Place it at `secrets/sa.json` on your Unraid host — never commit to GitHub --- ## Part 2 — Google Admin Console Authorization -This section delegates authority to the service account inside Google Admin, -allowing it to impersonate users and update their Gmail signatures. - -### Step 6 — Authorize the Service Account in Google Admin - -1. Open a new tab and go to https://admin.google.com -2. Sign in with your **Google Workspace super admin** account -3. In the left sidebar, navigate to: - **Security → Access and data control → API controls** -4. On the API controls page, click **Manage Domain Wide Delegation** -5. Click **Add new** to add a new authorized client - -Fill in the form: -- **Client ID:** Paste the numeric Client ID you copied in Step 4 - (find it again at GCP Console → IAM & Admin → Service Accounts → your account → Details) -- **OAuth Scopes:** Paste the following exactly, as one comma-separated line: - ``` - https://www.googleapis.com/auth/admin.directory.user.readonly,https://www.googleapis.com/auth/gmail.settings.basic - ``` +### Step 6 — Authorize Domain-Wide Delegation +1. Go to https://admin.google.com — sign in as super admin +2. Navigate to: **Security → Access and data control → API controls** +3. Click **Manage Domain Wide Delegation** +4. Click **Add new** +5. Fill in: + - **Client ID:** *(paste the number from Step 4)* + - **OAuth Scopes:** + ``` + https://www.googleapis.com/auth/admin.directory.user.readonly,https://www.googleapis.com/auth/gmail.settings.basic + ``` 6. Click **Authorize** -> If you receive an error saying "client ID not found", wait 2–3 minutes after -> enabling domain-wide delegation in Step 4 and try again — GCP propagation -> can take a moment. +> If you get "client ID not found" — wait 2–3 minutes after Step 4 and try again. ---- +### Step 7 — Verify -### Step 7 — Verify the Authorization - -1. After clicking Authorize you should see the new entry in the list -2. Click **View details** next to your entry -3. Confirm both scopes are listed: - - `https://www.googleapis.com/auth/admin.directory.user.readonly` - - `https://www.googleapis.com/auth/gmail.settings.basic` -4. If a scope is missing, click **Edit**, re-enter the full comma-separated scope string, and click **Authorize** again - -> **Multi-party approval note:** If your Google Workspace org has multi-party -> approval enabled for admin actions, another super admin will need to approve -> this delegation before it takes effect. +1. Click **View details** next to your new entry +2. Confirm both scopes are listed +3. If a scope is missing, click Edit, re-enter the full scope string, and re-authorize --- ## Part 3 — Unraid Deployment -### Step 8 — Clone the Repository - -SSH into your Unraid server or use the Unraid terminal: +### Step 8 — Clone the Repo ```bash cd /mnt/user/appdata -git clone https://github.com/YOURUSERNAME/email-sig-manager.git -cd email-sig-manager +git clone https://github.com/YOURUSERNAME/email-sig-manager.git email-sigs +cd email-sigs ``` ### Step 9 — Place the Service Account Key -Copy `sa.json` (downloaded in Step 5) into the `secrets/` folder: - ```bash -# From your PC to Unraid (run this on your PC): -scp sa.json root@UNRAID-IP:/mnt/user/appdata/email-sig-manager/secrets/sa.json +# From your PC (run on your PC): +scp sa.json root@UNRAID-IP:/mnt/user/appdata/email-sigs/secrets/sa.json +``` +Or copy via SMB: `\\UNRAID-IP\appdata\email-sigs\secrets\` + +### Step 10 — Upload Logo + +Upload your logo to `https://alwisp.com/uploads/logo.png` via your web server. +The template references this URL directly — no local file needed. + +### Step 11 — Build the Docker Image + +SSH into Unraid and run: +```bash +cd /mnt/user/appdata/email-sigs +docker build -t email-sigs:latest . ``` -Or copy it via your Unraid SMB share using Windows Explorer: -``` -\\UNRAID-IP\appdata\email-sig-manager\secrets\ -``` +### Step 12 — Configure Container in Unraid Docker UI -### Step 10 — Add Your Company Logo +- **Name:** `email-sigs` +- **Repository:** `email-sigs` *(local image built in Step 11)* -Place your logo file at: -``` -/mnt/user/appdata/email-sig-manager/public/assets/logo.png -``` -Recommended: PNG, transparent background, 160×160px or smaller. +**Volume mappings:** -### Step 11 — Configure Container in Unraid Docker UI - -1. In the Unraid web UI, go to **Docker → Add Container** -2. Set the following: - - **Name:** `email-sig-manager` - - **Repository:** *(leave blank if building locally — use the path method below)* - - **Network Type:** Bridge - -**Add these Volume mappings:** - -| Container Path | Host Path | +| Host Path | Container Path | |---|---| -| `/app/secrets` | `/mnt/user/appdata/email-sig-manager/secrets` | -| `/app/data` | `/mnt/user/appdata/email-sig-manager/data` | -| `/app/public/assets` | `/mnt/user/appdata/email-sig-manager/public/assets` | +| `/mnt/user/appdata/email-sigs/secrets` | `/app/secrets` | +| `/mnt/user/appdata/email-sigs/data` | `/app/data` | +| `/mnt/user/appdata/email-sigs/public/assets` | `/app/public/assets` | -**Add these Port mapping:** +**Port mapping:** Host `3000` → Container `3000` -| Container Port | Host Port | -|---|---| -| `3000` | `3000` *(or any open port)* | - -**Add these Variables** (click Add Variable for each): +**Variables:** | Name | Value | |---|---| -| `GOOGLE_ADMIN_EMAIL` | `admin@messagepointmedia.com` | +| `GOOGLE_ADMIN_EMAIL` | `jason@messagepoint.tv` | | `GOOGLE_CUSTOMER_ID` | `my_customer` | | `SERVICE_ACCOUNT_KEY_PATH` | `/app/secrets/sa.json` | | `ADMIN_USERNAME` | `admin` | @@ -241,57 +188,39 @@ Recommended: PNG, transparent background, 160×160px or smaller. | `CRON_SCHEDULE` | `0 2 * * *` | | `NODE_ENV` | `production` | -### Step 12 — Build and Start - -```bash -cd /mnt/user/appdata/email-sig-manager -docker-compose up -d --build -``` - --- ## Part 4 — First Run & Verification -### Step 13 — Access the Admin UI +### Step 13 — Access the UI -Open a browser and navigate to: ``` http://UNRAID-IP:3000 ``` -Log in with your `ADMIN_USERNAME` and `ADMIN_PASSWORD`. -### Step 14 — Test with a Single User First +### Step 14 — Preview and Test -1. Go to the **Template Editor** page -2. Verify the live preview renders correctly with your info -3. Make any template adjustments needed -4. Return to the **Dashboard** -5. In the **Push Single User** field, enter your own email address -6. Click **Push Single User** -7. Open Gmail in a new tab → compose a new email → verify the signature appears correctly -8. Check the Gmail mobile app as well (force-close and reopen if needed) +1. Go to **Template Editor** — verify the preview renders correctly +2. Back on **Dashboard** → enter your own email → **Push Single User** +3. Open Gmail → compose a new message → verify signature appears +4. Check Gmail mobile app (force-close and reopen if needed) ### Step 15 — Push to All Users -Once you've verified your own signature looks correct: - -1. Click **Push to All Users** on the Dashboard -2. Confirm the dialog -3. Watch the audit log populate with success/error statuses -4. The nightly cron will take over automatically from here +Once verified, click **Push to All Users** on the Dashboard. +The nightly cron will keep all signatures in sync automatically. --- -## Updating the Project +## Updating ```bash -cd /mnt/user/appdata/email-sig-manager +cd /mnt/user/appdata/email-sigs git pull -docker-compose up -d --build +docker build -t email-sigs:latest . +docker restart email-sigs ``` -`secrets/`, `data/`, and host-managed files are gitignored and unaffected by pulls. - --- ## Project Structure @@ -299,7 +228,7 @@ docker-compose up -d --build ``` email-sig-manager/ ├── src/ -│ ├── index.js # Express app entry +│ ├── index.js # Express app (no dotenv — vars injected by Docker) │ ├── routes/ │ │ ├── admin.js # Template, logs, users API │ │ └── push.js # Signature push logic + batch runner @@ -316,11 +245,10 @@ email-sig-manager/ ├── public/ │ ├── dashboard.html # Admin dashboard UI │ ├── editor.html # Template editor with live preview -│ └── assets/ -│ └── logo.png # Company logo (add your own) +│ └── assets/ # Local asset dir (optional, logo served remotely) ├── secrets/ # Gitignored — sa.json goes here ├── data/ # Gitignored — SQLite DB lives here -├── Dockerfile +├── Dockerfile # Uses npm install (not npm ci) ├── docker-compose.yml ├── package.json └── .env.example # Variable reference — safe to commit @@ -341,29 +269,33 @@ email-sig-manager/ ## Troubleshooting +**"Cannot find module" errors on startup** +Rebuild the image: `docker build -t email-sigs:latest .` then restart. + **Container won't start** -Run `docker logs email-sig-manager`. Most likely a missing env variable or malformed `sa.json`. +Run `docker logs email-sigs` — most likely a missing env variable or malformed `sa.json`. **"No primary sendAs" error** -The user may not have an active Gmail account, or domain-wide delegation scopes were not saved correctly in Google Admin. +The user may not have an active Gmail account, or domain-wide delegation scopes were not saved correctly. -**"Client ID not found" when authorizing in Google Admin** -Wait 2–3 minutes after enabling domain-wide delegation in GCP and try again. +**"Client ID not found" in Google Admin** +Wait 2–3 minutes after enabling domain-wide delegation in GCP, then try again. **Signatures not showing on mobile** -Gmail iOS/Android automatically uses the web signature. Have the user force-close and reopen the Gmail app after the push. +Gmail iOS/Android uses the web signature automatically. Force-close and reopen the app. **Template changes not saving** -Verify the container has write access to the `templates/` directory. A `.bak` file is created on every save. +Verify the container has write access to `templates/`. A `.bak` file is created on every save. -**401 / permission denied errors in logs** -The OAuth scopes in Google Admin may not have saved correctly. Go back to Admin Console → Security → API Controls → Domain Wide Delegation → View Details and verify both scopes are listed. +**401 / permission denied errors** +Go to Google Admin → Security → API Controls → Domain Wide Delegation → View Details +and verify both OAuth scopes are listed correctly. --- ## Security Notes - `sa.json` grants impersonation rights to every user in your domain — treat it like a master key -- Never commit `sa.json` or a populated `.env` to GitHub +- Never commit `sa.json` to GitHub — it is gitignored - Change `ADMIN_PASSWORD` before first deployment -- Consider placing the UI behind HTTPS if accessible outside your LAN +- Consider HTTPS via a reverse proxy if the UI is accessible outside your LAN diff --git a/docker-compose.yml b/docker-compose.yml index ec535ab..69834f1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,10 +1,6 @@ version: "3.8" # ───────────────────────────────────────────────────────────────────────────── -# Email Signature Manager -# -# Environment variables are passed in at runtime — not stored in this file. -# # On Unraid: Set each variable in the Docker container template UI # For local dev: Create a .env file (see .env.example) and run: # docker-compose --env-file .env up -d --build diff --git a/public/dashboard.html b/public/dashboard.html index 5f5d1b1..86a9a56 100644 --- a/public/dashboard.html +++ b/public/dashboard.html @@ -29,9 +29,9 @@ .badge.success { background: #1a4a1a; color: #4caf50; } .badge.error { background: #4a1a1a; color: #f44336; } .badge.skipped { background: #3a3a1a; color: #aaa; } - #status-msg { margin-bottom: 12px; padding: 10px 14px; border-radius: 6px; display: none; } - #status-msg.ok { background: #1a4a1a; color: #4caf50; display: block; } - #status-msg.err { background: #4a1a1a; color: #f44336; display: block; } + #status-msg { margin-bottom: 12px; padding: 10px 14px; border-radius: 6px; } + #status-msg.ok { background: #1a4a1a; color: #4caf50; } + #status-msg.err { background: #4a1a1a; color: #f44336; } input[type=email] { padding: 9px 12px; border-radius: 6px; border: 1px solid #555; background: #2a2a2a; color: #eee; font-size: 13px; width: 240px; } .ts { color: #777; font-size: 11px; } @@ -57,7 +57,7 @@ -
+ @@ -90,8 +90,10 @@ } function showStatus(msg, ok) { const el = document.getElementById('status-msg'); - el.textContent = msg; el.className = ok ? 'ok' : 'err'; - setTimeout(()=>{ el.className=''; el.textContent=''; }, 7000); + el.textContent = msg; + el.className = ok ? 'ok' : 'err'; + el.style.display = 'block'; + setTimeout(() => { el.style.display = 'none'; }, 7000); } async function pushAll() { if (!confirm('Push signatures to ALL users?')) return; diff --git a/public/editor.html b/public/editor.html index 3a259d5..54d5c74 100644 --- a/public/editor.html +++ b/public/editor.html @@ -15,7 +15,7 @@ .layout { display: flex; gap: 20px; flex-wrap: wrap; } .panel { flex: 1; min-width: 300px; } .panel h2 { font-size: 14px; color: #C9A84C; margin-bottom: 10px; } - textarea { width: 100%; height: 360px; background: #222; color: #eee; border: 1px solid #444; border-radius: 6px; padding: 12px; font-family: monospace; font-size: 12px; resize: vertical; } + textarea { width: 100%; height: 380px; background: #222; color: #eee; border: 1px solid #444; border-radius: 6px; padding: 12px; font-family: monospace; font-size: 12px; resize: vertical; } .preview-box { background: #fff; border-radius: 6px; padding: 20px; min-height: 120px; border: 1px solid #444; } .actions { display: flex; gap: 10px; margin-top: 16px; flex-wrap: wrap; } button { background: #C9A84C; color: #111; border: none; padding: 10px 20px; border-radius: 6px; font-weight: bold; cursor: pointer; font-size: 14px; } @@ -27,6 +27,9 @@ #status-msg.err { background: #4a1a1a; color: #f44336; display: block; } .test-fields { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; } .test-fields input { background: #2a2a2a; border: 1px solid #444; color: #eee; padding: 6px 10px; border-radius: 4px; font-size: 12px; width: calc(50% - 4px); } + .logo-row { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; } + .logo-row input { flex: 1; background: #2a2a2a; border: 1px solid #444; color: #eee; padding: 6px 10px; border-radius: 4px; font-size: 12px; } + .logo-row label { font-size: 12px; color: #aaa; white-space: nowrap; } @@ -50,11 +53,15 @@

Live Preview

- - - - - + + + + + +
+
+ +
Loading preview...
@@ -79,19 +86,20 @@ async function updatePreview() { const templateHtml = document.getElementById('template-editor').value; const userData = { - fullName: document.getElementById('p-name').value, - title: document.getElementById('p-title').value, - email: document.getElementById('p-email').value, - phone: document.getElementById('p-phone').value, - cellPhone: document.getElementById('p-cell').value + fullName: document.getElementById('p-name').value, + title: document.getElementById('p-title').value, + email: document.getElementById('p-email').value, + phone: document.getElementById('p-phone').value, + cellPhone: document.getElementById('p-cell').value, + logoUrl: document.getElementById('p-logo').value }; const res = await fetch('/api/admin/preview',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({templateHtml,userData})}).then(r=>r.json()).catch(()=>({error:'Preview failed'})); document.getElementById('preview-frame').innerHTML = res.html || `${res.error}`; } function showStatus(msg, ok) { const el = document.getElementById('status-msg'); - el.textContent = msg; el.className = ok ? 'ok' : 'err'; - setTimeout(()=>{ el.className=''; el.textContent=''; }, 5000); + el.textContent = msg; el.className = ok ? 'ok' : 'err'; el.style.display = 'block'; + setTimeout(()=>{ el.style.display='none'; }, 5000); } loadTemplate(); diff --git a/src/index.js b/src/index.js index 3df14f5..4db7916 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,3 @@ -require('dotenv').config(); const express = require('express'); const path = require('path'); const basicAuth = require('express-basic-auth'); diff --git a/src/routes/admin.js b/src/routes/admin.js index 3a26b37..b1a99ec 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -44,10 +44,10 @@ router.post('/preview', (req, res) => { const template = Handlebars.compile(templateHtml); const rendered = template(userData || { fullName: 'Jason Stedwell', - title: 'Director of Technical Services', - email: 'jstedwell@messagepointmedia.com', - phone: '334-707-2550', - cellPhone: '' + title: 'Director of Technical Services (IT/Systems/Engineering)', + email: 'jason@messagepoint.tv', + phone: '205-719-5000', + cellPhone: '334-707-2550' }); res.json({ html: rendered }); } catch (err) { diff --git a/src/services/googleAdmin.js b/src/services/googleAdmin.js index f49c921..140e66a 100644 --- a/src/services/googleAdmin.js +++ b/src/services/googleAdmin.js @@ -31,15 +31,15 @@ async function getAllUsers() { } while (pageToken); return users.map(u => ({ - email: u.primaryEmail, - fullName: u.name?.fullName || '', - firstName: u.name?.givenName || '', - lastName: u.name?.familyName || '', - title: u.organizations?.[0]?.title || '', + email: u.primaryEmail, + fullName: u.name?.fullName || '', + firstName: u.name?.givenName || '', + lastName: u.name?.familyName || '', + title: u.organizations?.[0]?.title || '', department: u.organizations?.[0]?.department || '', - phone: u.phones?.find(p => p.type === 'work')?.value || '', - cellPhone: u.phones?.find(p => p.type === 'mobile')?.value || '', - suspended: u.suspended || false + phone: u.phones?.find(p => p.type === 'work')?.value || '', + cellPhone: u.phones?.find(p => p.type === 'mobile')?.value || '', + suspended: u.suspended || false })); } diff --git a/templates/default.hbs b/templates/default.hbs index ba68fc1..fae5441 100644 --- a/templates/default.hbs +++ b/templates/default.hbs @@ -1,21 +1,27 @@ -
TimestampUserNameStatusMessage
Loading...
+
- -
- Messagepoint Media + + Messagepoint Media -

{{fullName}}

-

{{title}}

-

- {{email}} +

+

{{fullName}}

+ {{#if_val title}} +

{{title}}

+ {{/if_val}} + {{#if_val email}} +

+ {{email}}

+ {{/if_val}} {{#if_val phone}} -

O: {{phone}}

+

O: {{phone}}

{{/if_val}} {{#if_val cellPhone}} -

C: {{cellPhone}}

+

C: {{cellPhone}}

{{/if_val}}