How to configure Oracle11g to send out Password Expiry Notifications vide SMS or e-mail?
With all the possibilities of not expired, in grace period and expired this can be quite tricky.  As well many databases have service accounts that there is no point in sending emails to as they do not have an email address. In order to get this right you need:


a database profile for the people whose passwords expire and you wish to advise
a table to hold their database username, email, phone
a procedure or external process so that when a new user is added you add them to your email table.  Do not put a trigger on dba_users!
a logging table so when someone's password expires and they say they were not notified you can show when the email was sent
a table/package to hold constants like the name of the email domain 


I had this code created by someone on our team to answer the same problem. It works but could be improved.  I leave the profile, table and some package names as an exercise

PROCEDURE EmailExpiringPassword IS

      pEmailBody       VARCHAR2(32767);
      pEmailTitle      VARCHAR2(32767); 
      pCount           INTEGER;
      pDBName          VARCHAR2(50);
      pDomain          VARCHAR2(50);
      --email of group issuing this warning 
      --so they can be contacted IF the user is locked out
      pDBNameSupportEmail        VARCHAR2(255);  
begin
      pDomain := &lt;call a package to get the constant value for the domain name&gt;
      pDBNameSupportEmail := &lt;call a package to get the constant value for the email address of the group/team responsible for account management&gt;

      SELECT d.name
      into pDBName
      FROM v$database d;

       FOR missed IN (  SELECT
                        du.username,
                        du.expiry_date,
                        to_char(coalesce(abs(cast(du.expiry_date - sysdate as integer)),0)) as days,
                        du.account_status,
                        du.profile     
                        FROM dba_users du
                        left outer join &lt;Your custom table to hold user email,phone&gt; ud
                           on ud.db_username = du.username
                        where   ud.db_username is null
                        and du.profile in ('&lt;profiles of interest&gt;') 
                      )
         LOOP
          pCount := pCount + 1;
          pEmailTitle := pEmailTitle || missed.username || ' missing record, ';

          pEmailBody := pEmailBody ||
                        '&lt;tr&gt;' || 
                        'Username: ' || missed.username || '&lt;br&gt;' ||
                        'Profile: ' || missed.profile || '&lt;br&gt;' ||
                        'Expiry Date: ' || missed.expiry_date || '&lt;br&gt;' ||
                        'Account Status: ' || missed.account_status ||  '&lt;br&gt;' ||
                        'Database: ' || pDBName  || '&lt;br&gt;' ||
                        'Table: &lt;Your custom table to hold user email,phone&gt;' || '&lt;br&gt;' ||
                        'Missing record. Please add the user to the table &lt;Your custom table to hold user email,phone&gt;.'   || '&lt;br&gt;' ||
                        '' || '&lt;br&gt;' 
                        ;

          IF (length(pEmailBody) &gt; 30000)THEN
            EXIT;
          END IF;
         END LOOP;

         IF (pCount &gt; 0)
           THEN
           &lt;Your custom package to send email&gt;(
           strFROM    =&gt; pDBName || '_missing_record@' ||  pDomain ,                            
           strsubject =&gt; pEmailTitle,
           strbody    =&gt; pEmailBody,
           strto      =&gt; pDBNameSupportEmail
           );

           INSERT into &lt;Your custom table to hold user email,phone&gt;_PASSWORD_EMAIL_LOG
           (user_data_pwd_email_log_id ,sentdate, username, EmailAddress, EmailBody)
           SELECT (SELECT coalesce(max(user_data_pwd_email_log_id),0)+1 
           FROM &lt;Your custom table to hold user email,phone&gt;_PASSWORD_EMAIL_LOG),
           sysdate,
           pEmailTitle,
           pDBNameSupportEmail,
           pEmailBody
           FROM dual;
           commit;

         END IF;

         pCount := 0;

        FOR email IN (  SELECT
                        du.username,
                        du.expiry_date,
                        ud.firstdotlastname || '@' || pDomain as email, 
                        cast(du.expiry_date - sysdate as integer) as days_number,
                        to_char(coalesce(abs(cast(du.expiry_date - sysdate as integer)),0)) as days,
                        account_status, CASE WHEN p.limit = 'DEFAULT' THEN 10 ELSE 10 END AS Limit     
                        FROM &lt;Your custom table to hold user email,phone&gt; ud
                        inner join dba_users du
                             on ud.db_username = du.username
                        inner join dba_profiles p
                             on du.profile = p.profile 
                             and resource_name = 'PASSWORD_GRACE_TIME'
                        where 
                        du.profile in ('&lt;profiles of interest&gt;') 
                        and (cast(du.expiry_date - sysdate as integer) between -10 and 10
                            or account_status = 'EXPIRED (GRACE)')
                        and ud.active_ind = 1
                      )
         LOOP

          pCount := pCount + 1;

          pEmailTitle := email.username || ' password expiring';

          pEmailBody := 'Username: ' || email.username || '&lt;br&gt;' ||
                        'Database: ' || pDBName  || '&lt;br&gt;' || '&lt;br&gt;' ;

          IF NOT (email.account_status = 'EXPIRED (GRACE)' and email.expiry_date is null) and email.days_number&gt;0 
              THEN
                pEmailBody := 
                'Your password will expire in ' ||  email.days || ' days. ' ||
                'Please reset your password. It will expire after ' || 
                email.expiry_date || 
                ' and locked after ' || 
                to_char(email.expiry_date + email.limit) || '.' ||
                'Contact ' || pDBNameSupportEmail || ' for details.' ||'&lt;br&gt;' ||'&lt;br&gt;'; 


          ELSE IF email.days_number &lt;= 0 and email.expiry_date &gt;= sysdate
               THEN
                pEmailBody :=                
                'Your password expired ' ||  email.days || ' days ago. ' ||
                'Please reset your password. It will be locked after ' || 
                to_char(email.expiry_date + email.limit) || ' days.' ||
                ' Contact ' || pDBNameSupportEmail || ' for details.' ||'&lt;br&gt;' ||'&lt;br&gt;' ;       

           ELSE  
                pEmailBody := pEmailBody ||                 
                'Please login into ' || pDBName || ' to reset your password. ' || '&lt;br&gt;' ||'&lt;br&gt;';
           END IF;              
        END IF;   

          pEmailBody := pEmailBody ||
          '&lt;i&gt;' || 'Please do not reply to this email, this address is not monitored.'   ||                     
          '&lt;/tr&gt;'; 


         INSERT into &lt;Your custom table to hold user email,phone&gt;_PASSWORD_EMAIL_LOG
         (user_data_pwd_email_log_id ,sentdate, username, EmailAddress ,  EmailBody)
         SELECT (SELECT coalesce(max(user_data_pwd_email_log_id),0)+1 FROM &lt;Your custom table to hold user email,phone&gt;_PASSWORD_EMAIL_LOG),
         sysdate,
         email.username,
         email.email,
         pEmailBody
         FROM dual;
         commit; 

         &lt;Your custom package to send email&gt;(
         strFROM    =&gt; pDBName || '_password_expiring@' ||  pDomain ,                            
         strsubject =&gt; pEmailTitle,
         strbody    =&gt; pEmailBody,
         strto      =&gt; email.email
         );

         END LOOP;


-- delete logs 60+ days old
delete FROM PASSWORD_EMAIL_LOG
where cast(sysdate - sentdate  as integer) &gt;=60;

end EmailExpiringPassword;