Saturday, January 15, 2011

mail.app can't send mail using that server

Last October I migrated the lovely Carol's computing environment to a mac mini running snow leopard. Incoming mail was handled with an ssh tunnel to our ISP (localhost 60110 forwards to our ISP port 110), but I figured outbound mail didn't need anything that fancy, as our IP address should be on our ISP's list of "good" email addresses.

But lately we've been getting unhappy returns... mail.app would pop up with a "Can't send mail..." and would not tell me why. When the symptom appeared, I tried "telnet mail.ourISP.net 25" and manually-entered mail worked just fine. Wha...??

So this morning, when the symptom hit again, I sat down to do battle. I didn't see wireshark under Applications→Utilities; I didn't see ethereal. So it was time to go Neanderthal; I brought up a Terminal window and said

$ type tcpdump
tcpdump is /usr/sbin/tcpdump        ← Oh yeah, we're in business!
$ sudo tcpdump -w /tmp/tcpdump-mail-try1 -p 
tcpdump: listening on en0, link-type EN10MB (Ethernet), capture size 65535 bytes
Now I didn't actually hit <ENTER> on that tcpdump command until I had a mail message ready to send in mail.app. I hit <ENTER> to start off tcpdump, clicked "Send" in mail.app, got the nasty "Can't send using that server" window from mail.app, and then hit control-C in the Terminal window, thus interrupting tcpdump.

Next step was to copy the tcpdump output file to my OpenSUSE box and look at it with wireshark. A minute later I understood how this attempt failed, even though my "telnet" method worked fine.

Frame number 1 in the main window had the necessary information: we were connecting to our ISP's mail server, not using port 25 as I had thought, but using port 587. I clicked on Analyze&rarrFollow TCP stream and saw that my ISP was asking for authentication, which I didn't really want to send. But what is port 587?

collin@p3:~> grep 587 /etc/services
submission 587/tcp    # Submission
submission 587/udp    # Submission
… remainder elided
Submission, huh? Well, I shouldn't have to submit in this context; I want to use port 25. But how to change it? Google to the rescue! It led me this page, which shows how to tell mail.app to use a different port. Basically, it's Preferences, Accounts, Account Information, Server settings (outbound), Advanced, and in our version of mail.app, set "custom port" -- which I set to 25.

Saturday, January 08, 2011

Now there is no condemnation

Someone asked about a favorite Scripture, and Romans 8:1 (whence the title) immediately came to mind. Here is how I remember it (and the next few verses):
There is therefore now no condemnation for those who are in Christ Jesus. For the law of the Spirit of life in Christ Jesus has set us free from the law of sin and of death. For what the law could not do, weak as it was through the flesh, God did: sending his own son in the likeness of human flesh and (as an offering for) sin, he condemned sin in the flesh so that the righteous requirements of the law might be met in us, who do not walk according to the flesh but according to the Spirit.
Did I get it right? That should be Romans 8:1-4, NASB (biblegateway.com).

I wasn't always so excited about this passage. I mean, it's elementary, right? God sent Jesus his son so that our sins would be forgiven -- so we need not worry about meeting the requirements of the written code.

Elementary, yes, but that doesn't mean that it's boring. A couple of things happened in the decades since I read and memorized the passage (I see it's gotten rusty): one is I've become aware of more things in my soul that aren't right; the other -- well, maybe it's a subset of the first -- is that I see more clearly how much I need to be reminded of this very important truth. This morning I picked up The Message, where I read Peterson's paraphrase of this passage:

1-2With the arrival of Jesus, the Messiah, that fateful dilemma is resolved. Those who enter into Christ's being-here-for-us no longer have to live under a continuous, low-lying black cloud. A new power is in operation. The Spirit of life in Christ, like a strong wind, has magnificently cleared the air, freeing you from a fated lifetime of brutal tyranny at the hands of sin and death. more...
from Romans 8:1-4 MSG
Something I like about the Message is that it points even more emphatically at the end of chapter 7. The NIV and NASB both say "therefore", and there's a rule of Bible study that says when you see a "therefore", find out what it's there for. That alone should have told me to look at what came just before.

Yet seeing Peterson's words "With the arrival of Jesus, the Messiah, that fateful dilemma is resolved" really drove me to look back at Romans 7:25, which says Jesus "acted to set things right in this life of contradictions where I want to serve God with all my heart and mind, but am pulled by the influence of sin to do something totally different." (The Message)

Back to Romans 8:1-4, I also really like Peterson's reminder that spiritual transformation isn't a matter of "you suck, try harder." Here's the last paragraph, from Romans 8:3-4 (MSG):

The law always ended up being used as a Band-Aid on sin instead of a deep healing of it. And now what the law code asked for but we couldn't deliver is accomplished as we, instead of redoubling our own efforts, simply embrace what the Spirit is doing in us.
Romans 8:3-4, The Message (NIV here)
Indeed, it's not a matter of redoubling our efforts; did not the Lord himself say, "apart from me you can do nothing"? (John 15:5) Paul seems to think this is really important, too: "Don't be conformed to this world, but be transformed" (Romans 12:2); we're being transformed from glory to glory (2 Corinthians 3:18); the one who began a good work will complete it (Philippians 1:6); God himself will set us apart, sanctify us (1 Thessalonians 5:23-24).

And that sure sounds like good news to me.

Friday, January 07, 2011

Human being, or a human doing?

"... which is why we're called human beings, not human doings." That little sentence fragment came to mind when I read this from Thomas Merton the other night:
We are warmed by the fire, not by the smoke of the fire. We are carried over the sea by a ship, not by the wake of a ship. So too, what we are is to be sought in the invisible depths of our own being, not in our outward reflection in our own acts. We must find our real selves not in the froth stirred up by the impact of our being upon the beings around us, but in our own soul which is the principle of all our acts.
Merton, No Man Is an Island, p. 117
(chapter 7, Barnes & Noble 2003 edition)
Our pastor said the other day that the sickness of our time is that we are unaware of our souls and hence are unable to take proper care of them.

I think he's right about that. If my soul is what integrates everything in my life and makes it a life rather than a chaos of disparate drives, then having a healthy soul means that my mind, will, and emotions -- my professed beliefs and my actual desires -- will be aligned. I won't say one thing but actually wish another; what I "should want" would become what I actually want, and my "best self" would be what everyone around me sees every day.

Today's reality, of course, doesn't match that description -- in you or in me. David wrote, "Search me, O God, and know my heart; test me and know my anxious thoughts" (Psalm 139:23); his soul wasn't perfect, either, as shown by the adultery and murder he committed in 2 Samuel 11. Less dramatically, we can see David's internal conflict within Psalm 139 itself: after acknowledging that the Lord knows him (Ps. 139:1-6), he thinks about fleeing (139:7-11).

But it seems that our modern, high-tech world is so filled with distractions that most of us don't even know how conflicted we are -- toward God, toward our jobs, toward each other -- even toward ourselves. Fortunately, David shows us a step we can take: confess our needs and our anxious thoughts to God -- indeed, ask God to help us know what they are -- and determine to follow where He leads.

Monday, January 03, 2011

small-c cosmopolitan Low/No Fat Butternut Squash with Onion

This is pretty good; the ex-teenager made it tonight. When we started eating it, she said, "Oh, I forgot the olive oil."

"What?" I cried. "You mean this is low fat??"

It's almost a no-fat dish, actually. Here's how it's made.

  1. Preheat oven to 450°F.
  2. You're only going to use half of it, but you may as well microwave
    • one whole butternut squash
    about two minutes. (Now the other half will be easier to cut up.)
  3. Peel about half of said squash and cut it into 1" cubes; put the rest into the 'fridge.
  4. Cut up
    • a medium onion
    into not-too-small chunks. Maybe eighths as viewed from the poles, and cut at each tropic (24 chunks in all).
  5. Spread the onion and squash in a 9"x13" baking dish.
  6. Drizzle
    • about 2T molasses,
    • about 2T balsamic vinegar
    • about 3T soy sauce
    over the vegetables. These amounts aren't exact (no measuring utensil was used -- just mark-1 eyeball). The soy sauce came out a lot faster so maybe it was 4T?
  7. If you don't want low-fat, add some olive oil over all, but it's not needed. Really.
  8. Bake uncovered at 450°F about 20? minutes or until vegetables are tender and onion doesn't offend.
    • Stir it every 5 minutes or so while baking.
  9. Also optional: mix in
    • about 3T crushed pecans (walnuts would probably also work)
Should have taken a photo. I'm not sure about the timing, and I also don't know how it would affect the dish if you were to cover the baking dish.

Sunday, January 02, 2011

Jumble® and Python

So I was on the train the other day, looking at the daily jumble® in the paper (if you don't know how it works, have a look at their website). I had hit a mental block, trying to decode MUGPIN and only getting as far as "umping" (is it a verb, to "ump"?). I could have typed
$ grep '^......$' /usr/share/dict/words | grep m | grep u | grep g | 
    grep p | grep i | grep n
(which would give me a list of six-letter words in the system-supplied dictionary containing all the letters m,u,g,p,i,n) but I thought, "Hey, I have a little time; I should write a script." That way, rather than typing all those "grep"s, I could type the name of the script ("anagram", say) and the letters of interest (in this case mugpin).
For the impatient: →put your mouse here← to see the answer.
What did all those greps mean?

The first one, "grep '^......$' /usr/share/dict/words", selects lines containing six characters: ^ means beginning-of-line, . matches anything other than end-of-line, and $ matches end-of-line.

The | ("pipe" or vertical bar) means: take the output of the prior command, and feed it to the following command as its input.

The next six greps select lines containing one or more 'm's, one or more 'u's, etc. And it gives the right result, but it's icky having to type "grep" so many times.

How best to write a script for this? There is probably some nice way to get bash(1) to give me a list of letters in a word, but I didn't know what it was. Besides that, I'm trying to write more Python and less bash. It turns out that Python treats a string like a list -- even more so if you say "list('whatever')"
$ python
Python 2.6.5 (r265:79063, Jul  5 2010, 11:47:21) 
[GCC 4.5.0 20100604 [gcc-4_5-branch revision 160292]] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> list('whatever')
['w', 'h', 'a', 't', 'e', 'v', 'e', 'r']
>>> 
My thought was, grab the list of words from /usr/share/dict/words, select the ones of the right length; from those, select the ones that contain all the letters in the supplied word. An initial whack might look something like this:
#!/usr/bin/python -utt
import sys              # for argv

def main(aword):
    dictwords = [X.strip() for X in open('/usr/share/dict/words', 'r')]

    aword = aword.strip().lower()
    awordlen = len(aword)
    somewords = [X for X in dictwords if len(X) == awordlen]
    for aletter in list(aword):
        somewords = [X for X in somewords if aletter in X]
    print aword, somewords

if __name__ == '__main__':
    main(sys.argv[1])

OK, let me explain a little. First, the #! line -- that tells the computer (if you're on a Linux® or Unix™-like system) to run the Python interpreter; the "-utt" says to unbuffer output (I'm so impatient) and to treat tabs as illegal for indentation -- safety first.

Then comes "main", which does the work of this script, takes a single input parameter, the scrambled word in question ('mugpin' in this case). The first thing the script does is get the word-list from the system-supplied dictionary, /usr/share/dict/words (if that file doesn't exist on your system, then I don't know where to get a nice word list from). I've become a fan of list comprehensions (the line beginning dictwords = for example) because I can say a lot in just one line without having it look like too much magic: what this line does is open the dictionary and put the blank-stripped version of each line into the array dictwords.

The script unblanks and downshifts the supplied word (that's what aword.strip().lower() does); it then calculates the resulting length. Then comes another list comprehension: somewords = [X for X in dictwords if len(X) == awordlen] -- this basically takes all words in dictwords of a certain length (the length of aword) and stores this subset into somewords

The loop ("for aletter in list(aword)") further narrows somewords down to only those words in the dictionary that have the letters in the supplied parameter. Finally, main() prints the supplied parameter and the list of matching words.

About the last two lines -- I never (well, hardly ever) write a Python script that just starts running, because when you try to run them under pdb they just run; generally the main part of the script goes into a routine main(), which is called only if this script is the main one being run. The last line passes sys.argv[1] as main()'s one parameter. Now to run in a debugger, I can type this:

% python
Python 2.3.5 (#1, Jan 12 2009, 14:43:55) 
[GCC 3.3 20030304 (Apple Computer, Inc. build 1819)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import pdb
>>> import ana2
>>> ana2.main('umping')
umping ['impugn']
>>> 
The script was saved in a file "ana2.py", so "import ana2" brought it "into" the Python interpreter. Here I just ran it, and it worked (casting caution to the wind and all that). If I wanted to do something a little more careful, I could type:
>>> pdb.run('ana2.main("umping")')
> (1)?()
(Pdb) s
--Call--
> /Users/collin/ana2.py(4)main()
-> def main(aword):
(Pdb) n
> /Users/collin/ana2.py(5)main()
-> dictwords = [X.strip() for X in open('/usr/share/dict/words', 'r')]
(Pdb) n
> /Users/collin/ana2.py(7)main()
-> aword = aword.strip().lower()
(Pdb) c
umping ['impugn']
>>> 
Here I told the debugger to step into the routine I gave it, which it did -- gelling me I was at line 4 of ana2.py; I then said to go to the next statement, line 5 -- which puts the whitespace-stripped list of lines from the dictionary into dictwords, and then to just continue -- then it printed the word I couldn't figure out.

But this naive script doesn't work so well when multiple letters are involved. Consider this embarrassment:

% ./ana2.py account
account ['account', 'auction', 'caution', 'conatus', 'courant', 'outcant']
Oops! A word like "auction" includes some letters besides the original, but "outcant" contains all and only the letters from the original word. We have to do a little more.

The basic idea I came up with was to find the duplicate letters (there may be more than one, as in "balloon" or "lightning"), and look for multiple occurrences of those multiply-occurring letters. As far as finding multiple letters, there are lots of ways to do it; I just sorted the letters and then looked for multiples in adjacent letters. Since there are only 26 letters it might be more efficient to zero an array of letters and look for collisions, but this way seems to work.

While I was at it, I decided to make the input case-insensitive by forcing the input to lowercase. Also I allowed multiple words to be given on the command line. And I added a few comments. Here's the next version:

#!/usr/bin/python -utt
# vim:et:sw=4
'''Usage:
\t%PROG% {word...}

For each _word_ given, print a list of its anagrams found
in the words file, /usr/share/dict/words .
'''

import re
import sys

DEBUG = False
LETTERSONLY = re.compile('[a-z]*$')

__doc__ = __doc__.replace('%PROG%', sys.argv[0])

def usage():
    print "Usage:", sys.argv[0], "{word...}"
    sys.exit(1)

def main(wordlist):
    if len(wordlist) == 0:              # Nothing to do, so don't do it.
        usage()

    # Fetch the words from the system-supplied dictionary
    dictwords = [X.strip() for X in open('/usr/share/dict/words','r')]

    # Screen each _word_, then look for anagrams
    for aword in wordlist:
        aword = aword.strip().lower()
        if LETTERSONLY.match(aword) is None:
            print "%%% Words must consist only of letters:", aword
            continue

        # The overall plan is: find words from dictwords[] that have 
        # the same length as _word_, and have all its letters.
letters = list(aword) mylen = len(letters) letters.sort() # letters[] is the letters in _word_, in ascending alphabetical order. # Now look for duplicates. Without handling duplicates, one might # end up with "abalone" or "albinos" or "ballons" when the user # typed "balloon" prevlet = '' dups = dict() for alet in list(letters): if alet == prevlet: # It's a dup. letters.remove(alet) # remove the dup if alet in dups: dups[alet] += 1 # increase the count else: dups[alet] = 2 prevlet = alet if DEBUG: print "DEBUG: letters", letters print "DEBUG: dups", dups
# Find the words from the dictionary that have the same length somewords = [X for X in dictwords if len(X) == mylen] # ... and have all the letters in the supplied _word_ for alet in letters: somewords = [ X for X in somewords if alet in X ] if DEBUG: print "DEBUG:", len(somewords), "candidates:", somewords # ... and have enough copies of each duplicate letter for duplet,dupcount in dups.items(): patstr = '' for junk in range(dupcount): patstr += ('.*' + duplet) pat = re.compile(patstr) somewords = [X for X in somewords if pat.match(X)] if DEBUG: print "DEBUG: after matching", patstr, ':', somewords print aword + ':', somewords sys.exit(0) if __name__ == '__main__': main(sys.argv[1:])
What's with the text that looks like this? Well, as I was writing about "sort" above, I began to feel a little embarrassed about how un-pythonic the above version was, so I hacked it. This background color indicates text that I changed in the version you see below.

Final(?) version

This one is about 14 lines shorter and doesn't bother with sorting. Changed text is indicated with this background color.
#!/usr/bin/python -utt
# vim:et:sw=4
'''Usage:
\t%PROG% {word...}

For each _word_ given, print a list of its anagrams found
in the words file, /usr/share/dict/words .
'''

import re
import sys

DEBUG = False
LETTERSONLY = re.compile('[a-z]*$')

__doc__ = __doc__.replace('%PROG%', sys.argv[0])

def usage():
    print "Usage:", sys.argv[0], "{word...}"
    sys.exit(1)

def main(wordlist):
    if len(wordlist) == 0:              # Nothing to do, so don't do it.
        usage()

    # Fetch the words from the system-supplied dictionary
    dictwords = [X.strip() for X in open('/usr/share/dict/words','r')]

    # Screen each _word_, then look for anagrams
    for aword in wordlist:
        aword = aword.strip().lower()
        if LETTERSONLY.match(aword) is None:
            print "%%% Words must consist only of letters:", aword
            continue

        # The overall plan is: find words from dictwords[] that have 
        # the same length as _word_, and have all its letters.
mylen = len(aword) # Handle duplicates: _letters_ will map each letter to its count # e.g., for 'off', letters will have 'f':2, 'o':1 letters = dict() for alet in list(aword): letters[alet] = letters.get(alet,0) + 1
# Find the words from the dictionary that have the same length somewords = [X for X in dictwords if len(X) == mylen] # ... and have all the letters in the supplied _word_ for alet in letters: somewords = [ X for X in somewords if alet in X ] if DEBUG: print "DEBUG:", len(somewords), "candidates:", somewords # ... and have enough copies of each duplicate letter for duplet,dupcount in [X for X in letters.items() if X[1] > 1]: patstr = '' for junk in range(dupcount): patstr += ('.*' + duplet) pat = re.compile(patstr) somewords = [X for X in somewords if pat.match(X)] if DEBUG: print "DEBUG: after matching", patstr, ':', somewords print aword + ':', somewords sys.exit(0) if __name__ == '__main__': main(sys.argv[1:])
Call me (off)-white and nerdy, but that was fun!

Saturday, January 01, 2011

20% of peak internet traffic is Netflix streaming? And what about net neutrality?

This article on gizmodo.com says yes: "A recent study says that 20 percent of all downstream internet traffic during peak times in the US is Netflix streaming." The study quoted is here, and a 2009 report says that video-on-demand is about 27% of global internet traffic (never mind peak downstream bandwidth).

I'm no expert on network traffic, but this makes me wonder about net neutrality. I know the analogy isn't quite apt, but I think back to the old days, when one big expensive central processing unit served lots of users "simultaneously." In those days, one might imagine a very long process (a simulation of tomorrow's weather, for example) competing for CPU bandwidth with a very short process (an undergraduate programming assignment that typically would include an aborted compilation).

In those days, operating systems would tend to give short, interactive tasks a higher execution priority than long-running batch-oriented tasks. So if your weather simulation took many hours to run, you'd typically schedule a cheaper (yes, they used to charge users for computer time!) class of service -- you wanted to pay a low per-CPU-second rate -- that would nevertheless let your task use a lot of memory and CPU time, but would let other users (like the undergraduate who hasn't yet mastered the programming language's syntax, or the 72-character line length limit) slip in for a few seconds with quick turnaround.

Likewise, the undergraduates were given a class of service that would give them quick turnaround but with strict limits on how much they could use in the way of computing resources -- CPU-seconds, pages printed, memory, access to files stored on disk, etc.

But what do we do in this new world where during peak times, 20% of all downstream traffic is consumed by Netflix streaming? Is it reasonable that these bytes get the same treatment as the bytes of the short email I'm trying to send, or the blog post I'm trying to read?

Typically, interactive traffic used to involve small amounts of CPU time; lots of CPU time meant something that one knew was going to take a long time and hence didn't involve high "urgency." If we bring the analogy to today, where a big download (of, say, the OpenSUSE 11.3 DVD image) involves a lot of kbytes (not CPU-seconds), vs interactive traffic (when you click "recalculate total" on a merchant's website for example) which involves fewer kbytes but with more stringent latency requirements.

But when we're talking about interactive video (lots of kbytes with stringent latency requirements) or streaming movies (REALLY lots of kbytes, albeit with slightly less stringent latency issues -- that "buffering..." business), I have to say that if a network provider wanted to charge a higher rate for high-volume high-urgency service, that wouldn't seem all that unreasonable to me.

If it costs Netflix less than a dime to stream a two-hour HD movie (3 Gbytes), and it suddenly cost them more like a quarter to stream it -- vs what, 88 cents to ship a DVD to your house and for you to ship it back to them -- well, that wouldn't seem all that unreasonable either.

NOTE: I'm not in the employ of any network infrastructure outfits, nor have I received any bribes or fees for writing this. Nor am I a netflix hater.

Herod and Jesus -- Matthew 2

Matthew doesn't tell us a whole lot about the birth of Jesus -- "no room at the inn," the shepherds and angels -- all that comes from Luke. Matthew does, however, locate the birth of Jesus in space and time -- Jesus was born in Bethlehem, the city of David, during the reign of King Herod.

The next thing Matthew tells us is that Magi came from the east to Jerusalem. Why Jerusalem? They were looking for "the one who has been born king of the Jews" (Matthew 2:2), so naturally they went to the capital.

How many magi were there? Tradition says there were three, but I suppose there were several more. For one thing, consider a journey from present-day Iran to Jerusalem; these men would have had a long ways far from civilization. How large a party would you want to have, knowing that you might encounter bands of robbers? Not just three!

Then there's the matter of their political impact: King Herod meets with them and, Matthew tells us "he was disturbed, and all Jerusalem with him."

Question: How could three strangers from a foreign country shake up a whole city? To me it's more reasonable to suppose that a sizable contingent appeared; we're likely not talking about three wackos stirring up the entire city.

I find it really interesting to consider what Herod does. To find out where the king is to be born, Herod consults priests and legal scholars to ask them where the Christ (note that the magi refer to "born king of the Jews" and Herod equates this with "the Christ").

“In Bethlehem in Judea,” they replied, “for this is what the prophet has written:

“‘But you, Bethlehem, in the land of Judah, are by no means least among the rulers of Judah; for out of you will come a ruler who will be the shepherd of my people Israel.’”

Matthew 2:5-6, quoting Micah 5:2
Herod heard the name of the city but didn't pay enough attention to the rest of the quote. He apparently thought the prophet said about Bethlehem, "out of you will come forth somebody who will get killed before he can become a ruler to shepherd my people Israel."

How silly! How could he think God would get the city right but not the arc of Messiah's life? This guy had some real listening comprehension problems. He reminds me of a lot of other silly people we read about in the Bible, people who apparently never read any Greek tragedies. They think they can outsmart God.

As our pastor said, if we're going to say "Jesus is Lord" we also have to say "Jesus is smart" and that he really does know what he's saying. (By the way, quite a bit of this essay I owe to Pastor John.) Which means that it's best for me to do absolutely everything he says. Which I don't (as 'fessed up here among other places). Which is a way to say Herod also reminds me of me. Not a happy thought.

A recent sermon points out that the phrase "King Herod" is used only in verses 1 and 3; afterwards (seven more times in Matthew 2) he's referred to as simply "Herod"; "Then Herod called" (v.7), "not to go back to Herod" (v.12), "for Herod is going to search for the child" (v.13), etc. It's as if, once he decided to try to kill Messiah, Matthew no longer recognizes Herod as king; he's simply "Herod." Historians call him "Herod the Great" but really he's just Herod the silly.

Back to the Magi for a moment. That the star could lead them to a particular house in Bethlehem (Matthew 2:9-11) suggests to me that the "star" was something in the atmosphere, as even a planetary conjunction would be way too far away to indicate a village, let alone a particular house. They bring Jesus gifts: gold, incense and myrrh. As I understand it, the gifts correspond to the fact that Jesus is king (gold) and God (incense) and sacrifice (myrrh).

The Magi return home by another way and Herod goes bonkers, ordering his troops to kill all boys 2 years old and under in Bethlehem and the surrounding area. Why two years old and under? The Magi told Herod the exact time the star appeared (Matthew 2:7) and Herod could do arithmetic. Exactly how old was Jesus at this time? Well, the Magi saw "the child" rather than "the newborn" -- so probably they didn't come the same night as the shepherds. Luke tells us (Luke 2:39) that Joseph and Mary did everything required by the Law, which means they hung around Bethlehem/Jerusalem forty days at least. Luke doesn't mention the flight to Egypt (Matthew 2:13-14), and I don't know how precise Luke is being when he tells us that they returned (via Egypt maybe?) "when" they had done everything required by the Law. Anyway, Herod's order probably caused (where did I read this?) about two dozen boys to be killed -- Bethlehem was a small village, and even the surrounding area wouldn't include a whole lot of people.

A final note on the phrase "during the time of King Herod" -- this doesn't just refer to the time; it's like "during the McCarthy era" or "the Stalin years" -- it means a particularly dark time. Which it was.

I really recommend the sermon referred to above, particularly if your Christmas wasn't particularly merry.